Compare commits
180 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99cbfeef11 | |||
| ed1cb507cf | |||
| f4fab93a6c | |||
| 9b9f60a071 | |||
| d219176609 | |||
| 7411038b3b | |||
| 77ee608094 | |||
| 9bee2f233e | |||
| c317147ca4 | |||
| 15222b5453 | |||
| 6f15ada447 | |||
| 8b294d947d | |||
| 66c8838379 | |||
| 022ffccccd | |||
| 8ab6ab1c2a | |||
| 46d993e47f | |||
| 62f9472695 | |||
| b94984043c | |||
| fd78404d72 | |||
| 884ccc0db5 | |||
| 8ecc9fbe52 | |||
| d0edfa2538 | |||
| e1bf4d8454 | |||
| 5a08361f83 | |||
| 5d6d9efd27 | |||
| 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 |
@@ -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(github.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
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ run-name: Build & Publish by @${{ github.actor }}
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'platform/**'
|
||||
@@ -33,18 +35,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 +56,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,8 +116,9 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.REGISTRY_INTERNAL }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=sha,format=long
|
||||
type=sha,format=long,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -137,9 +132,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 }}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Feature Build — Windows MSI (via Conveyor)
|
||||
on:
|
||||
workflow_dispatch: # Nur noch manueller Start möglich, da ARM64-Runner inkompatibel
|
||||
# push:
|
||||
# branches: [ "feature/*" ] # Deaktiviert wegen ARM64 Exec Format Error
|
||||
|
||||
jobs:
|
||||
package-windows:
|
||||
name: 📦 Windows .msi Packaging
|
||||
# Desktop-CI ist nun via Conveyor auf Linux möglich
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup JDK 21 (Temurin)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
cache: gradle
|
||||
|
||||
- name: Gradle Build (Uber-JAR)
|
||||
run: |
|
||||
./gradlew :frontend:shells:meldestelle-desktop:jvmJar --no-daemon
|
||||
ls -lh frontend/shells/meldestelle-desktop/build/libs/
|
||||
|
||||
- name: Setup Conveyor
|
||||
run: |
|
||||
# Conveyor-Installation via Debian-Paket (stabiler in CI)
|
||||
sudo apt-get update && sudo apt-get install -y curl
|
||||
# Wir nutzen die offizielle Empfehlung für Debian-basierte Systeme
|
||||
curl -L https://conveyor.hydraulic.dev/install.sh -o install-conveyor.sh
|
||||
# Validierung: Wenn es kein Shell-Skript ist (sondern HTML), abbrechen
|
||||
if grep -q "<!DOCTYPE HTML" install-conveyor.sh; then
|
||||
echo "Fehler: Download-URL lieferte HTML statt Skript. Nutze npm-Fallback."
|
||||
npm install -g @hydraulic/conveyor
|
||||
else
|
||||
chmod +x install-conveyor.sh
|
||||
./install-conveyor.sh
|
||||
fi
|
||||
echo "$HOME/.conveyor/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Windows .msi mit Conveyor bauen
|
||||
run: |
|
||||
# HINWEIS: Erfordert aktuell einen x64-Linux-Runner.
|
||||
# Schlägt auf ARM64 (Zora) mit 'Exec format error' fehl.
|
||||
CONVEYOR_BIN=$(which conveyor || echo "$HOME/.conveyor/bin/conveyor")
|
||||
$CONVEYOR_BIN make windows-msi
|
||||
|
||||
- name: .msi Artefakt hochladen
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: meldestelle-windows-feature-build
|
||||
path: output/*.msi
|
||||
if-no-files-found: error
|
||||
@@ -4,6 +4,8 @@ on:
|
||||
branches: [ "**" ]
|
||||
jobs:
|
||||
no-hardcoded-versions:
|
||||
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
|
||||
if: ${{ !contains(github.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(github.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
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(github.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: tag-release
|
||||
|
||||
@@ -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(github.event.head_commit.message, '[planb]') }}
|
||||
runs-on: windows-latest
|
||||
needs: tag-release
|
||||
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# 🐧 [DevOps Engineer] Optimierte .gitignore für Meldestelle (KMP / Gradle / Docker)
|
||||
|
||||
# --- 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/
|
||||
@@ -3,12 +3,15 @@
|
||||
Dieses Dokument definiert die Zusammenarbeit zwischen dem User (Owner) und den spezialisierten KI-Agenten.
|
||||
Es dient als zentraler **System-Prompt-Erweiterung** für neue Chat-Sessions.
|
||||
|
||||
## 🚀 Strategische Ausrichtung
|
||||
## 🚀 Strategische Ausrichtung (Reality-Reset 28.04.2026)
|
||||
|
||||
Das Projekt **"Meldestelle"** entwickelt eine ÖTO/FEI-konforme, offline-fähige Turnier-Software.
|
||||
1. **Desktop-First:** Primäres Ziel ist die Compose Desktop App (KMP). UX & Performance sind auf Profis optimiert.
|
||||
2. **Offline-First:** Das System muss autark (ohne Internet) funktionieren. Sync-Logik ist Kernbestandteil.
|
||||
3. **Domain-Driven:** 6 Bounded Contexts (SCS) bilden den fachlichen Rahmen.
|
||||
2. **Offline-First:** Das System muss autark (ohne Internet) funktionieren.
|
||||
3. **Domain-Driven:** Die Hierarchie **Veranstaltung -> Turnier -> Bewerb/Abteilung** ist das absolute Fundament.
|
||||
|
||||
**WICHTIG:** Alle Agenten arbeiten ab sofort nur noch auf Basis von verifiziertem Code. "Halluzinationen" über
|
||||
abgeschlossene Phasen ohne entsprechende Implementierung sind untersagt.
|
||||
|
||||
## 1. Protokoll & Rollen-Badges
|
||||
Jede Agenten-Antwort **muss** mit dem entsprechenden Badge beginnen, um den Kontext und die Verantwortlichkeit zu klären.
|
||||
@@ -37,7 +40,9 @@ Jede Agenten-Antwort **muss** mit dem entsprechenden Badge beginnen, um den Kont
|
||||
4. **Doku-as-Code:** Änderungen an Code/Architektur müssen sofort in `docs/` (ADR/Reference) reflektiert werden.
|
||||
5. **Session-Abschluss:** Jede Session endet mit einem Eintrag durch den **Curator** (Journal oder Artefakt).
|
||||
|
||||
## 3. Projekt-Philosophie
|
||||
* **Information Density over White Space:** Wir bauen ein Profi-Werkzeug, kein Spielzeug.
|
||||
* **Speed over Animation:** Reaktionsgeschwindigkeit der UI hat höchste Priorität.
|
||||
* **Offline-Authentizität:** Lokale Daten sind die "Source of Truth" für den User; der Server ist das Backup/Sync-Target.
|
||||
## 🚫 Anti-Halluzinations-Protokoll (WICHTIG)
|
||||
Um Fehlentscheidungen und falsche Status-Meldungen zu verhindern, gelten ab sofort folgende Regeln:
|
||||
1. **Kein "Erledigt" ohne Beweis:** Ein Task darf erst dann als abgeschlossen markiert werden, wenn ein Test-Log, ein erfolgreicher Build oder eine explizite Bestätigung des Users vorliegt.
|
||||
2. **Status "Verifikation ausstehend":** Code, der geschrieben, aber nicht auf Hardware getestet wurde, muss zwingend diesen Zusatz tragen.
|
||||
3. **Fakten-Check vor Abschluss:** Vor dem Senden der `submit`-Meldung muss der Agent prüfen: "Habe ich das wirklich laufen sehen oder nehme ich es nur an?"
|
||||
4. **Fehler-Eingeständnis:** Bei Entdeckung einer Halluzination ist sofort der User zu informieren und der Status in allen Dokumenten (Roadmap, Journal) zu korrigieren.
|
||||
|
||||
+10
-207
@@ -17,216 +17,19 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **Onboarding & Desktop-UX - 15.04.2026:**
|
||||
- **Desktop-App:** Dynamisierung der Statusanzeigen im App-Footer ("Cloud synchronisiert" & "Verbunden").
|
||||
- **Connectivity-Tracking:** Implementierung des `ConnectivityTracker` (KMP) zur Echtzeit-Überwachung der API-Gateway
|
||||
Erreichbarkeit.
|
||||
- **LAN-Erkennung:** Integration des `NetworkDiscoveryService` (mDNS) im Footer zur Anzeige aktiver Instanzen im
|
||||
lokalen Netzwerk.
|
||||
- **Onboarding:** Datenfluss vom `SettingsManager` bis in den Footer finalisiert (Anzeige des echten Gerätenamens).
|
||||
- **Online-Nennung & Integration - 15.04.2026:**
|
||||
- **Backend (Mail-Service):** Finalisierung des `MailController` für Web-Nennungen inkl. SMTP-Versand via World4You.
|
||||
- **Frontend (Desktop):** `NennungsEingangScreen` an Live-Daten vom `mail-service` angebunden.
|
||||
- **Repository:** `NennungRemoteRepository` (KMP) um `holeNennungen()` erweitert.
|
||||
- **Billing & ÖTO - 15.04.2026:**
|
||||
- **Sportförderbeitrag:** Automatische Buchung von 1,00 EUR (§16 ÖTO) bei jeder Nennung im `entries-service`
|
||||
implementiert.
|
||||
- **Basis-Infrastruktur & Domain-Definition:**
|
||||
- DDD-Modelle für `Veranstaltung`, `Turnier`, `Bewerb` und `Abteilung` gemäß ÖTO definiert.
|
||||
- ZNS-Parser Prototyp für Dateiformate (VEREIN01, LIZENZ01, PFERD01, RICHT01).
|
||||
- Plan-B Mail-Service (Spring Boot) für Nennungs-Versand via World4You.
|
||||
- Desktop-App Skelett mit Navigation und UI-Hüllen (Compose Desktop).
|
||||
|
||||
### Behoben
|
||||
### Reality-Reset (28.04.2026)
|
||||
|
||||
- **Identity-Modul:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen und Behebung von
|
||||
Persistenz-Konflikten im `ExposedDeviceRepository`.
|
||||
- **Koin DI:** Korrektur von Typ-Inferenz-Fehlern beim `HttpClient` im `nennung-feature` durch explizite Qualifier.
|
||||
- **Turnier-Feature:** Behebung eines unsicheren Casts (`Any!` zu `List<String>`) in `TurnierStammdatenTab.kt`.
|
||||
- **Konfiguration:** Harmonisierung der Ports (Mail-Service auf 8083) in `.env`, `dc-backend.yaml` und
|
||||
`PlatformConfig.jvm.kt`.
|
||||
- **Korrektur:** Vormalige Einträge über "abgeschlossene" Billing-, Results- und Zeitplan-Features wurden entfernt, da
|
||||
diese im Code nicht funktional hinterlegt waren.
|
||||
- **Status:** Fokus zurück auf die Kern-Hierarchie (Veranstaltung -> Turnier -> Bewerb).
|
||||
|
||||
### Hinzugefügt
|
||||
- **Phase 12 (Abrechnung & Infrastruktur) - 12.04.2026:**
|
||||
- **Infrastruktur:** Docker-Integration für `billing-service` (Port 8087) und API-Gateway Routing vervollständigt.
|
||||
- **Service Discovery:** Alle relevanten Microservices (`masterdata`, `events`, `results`, `series`, `billing`) sind nun bei Consul registriert.
|
||||
- **Frontend Billing:** `BillingRepository` und `BillingViewModel` auf reale API-Anbindung (Ktor) umgestellt; `BillingScreen` funktionalisiert.
|
||||
- **Backend (Series):** JPA-Entitäten `Serie` und `SeriePunkt` im `series-service` stabilisiert und Flyway-Migrationen für das Datenbankschema erstellt.
|
||||
- **Fix:** Behebung von IDE-Mapping-Warnungen durch explizite `@Column` Namen in den JPA-Entitäten.
|
||||
- **Backend Fixes - 12.04.2026:**
|
||||
- **Infrastruktur:** Behebung von Startfehlern im `events-service` (DataSource) und `masterdata-service` (Consul).
|
||||
- **Build:** Integration von `results-service` und `series-service` in `settings.gradle.kts`.
|
||||
- **Domain:** `Serie` und `SeriePunkt` zu `data class` konvertiert (copy() Unterstützung).
|
||||
- **Phase 11 (Ergebniserfassung & Platzierung) - 12.04.2026:**
|
||||
- **Backend (Results):** `results-service` um JPA-Entitäten, Repositories und Business-Logik für Platzierungsberechnungen (Wertnote, Zeit, Fehler) ergänzt.
|
||||
- **Infrastructure:** `dc-backend.yaml` und `GatewayConfig.kt` um den Service `results` (Port 8088) erweitert.
|
||||
- **Frontend Domain:** `ErgebnisRepository` und `Ergebnis`-Modell für Wertnoten, Zeiten und Status erstellt.
|
||||
- **Frontend UI:** `ErgebnisEditDialog` zur schnellen Ergebniserfassung hinzugefügt; `TurnierStartlistenTab` ermöglicht nun Erfassung per Zeilen-Klick.
|
||||
- **Frontend UI:** `TurnierErgebnislistenTab` vervollständigt: Buttons für "Platzierung berechnen" und "Drucken" (PDF) funktionalisiert.
|
||||
- **Fix:** Kompilierungsprobleme im `TurnierFeatureModule` und `ScreenPreviews.kt` behoben (fehlende `ergebnisRepo` Parameter).
|
||||
|
||||
### Hinzugefügt
|
||||
- **Phase 10.4 (Series-Context Vertiefung) - 12.04.2026:**
|
||||
- **Backend (Series):** `series-service` um Logik für Streichresultate (`ReglementTyp`) und Bindungsarten (Reiter-zentriert, Pferde-zentriert, Paar-Bindung) erweitert.
|
||||
- **Infrastructure:** `dc-backend.yaml` und `GatewayConfig.kt` um den Service `series` (Port 8089) erweitert.
|
||||
- **Frontend Domain:** `SeriesRepository` und Modelle an das neue Ranking-Format (`SerieStandEntry`) angepasst.
|
||||
- **UI:** `SeriesScreen.kt` überarbeitet: Zeigt nun Reiter- und Pferde-IDs sowie Fortschritt pro Teilnehmer an.
|
||||
- **Dokumentation:** `MASTER_ROADMAP.md` aktualisiert (Phase 10 & 11 auf 'Completed' gesetzt).
|
||||
|
||||
### Hinzugefügt
|
||||
- **Phase 10.3 (Echter Datenverkehr & Infrastruktur) - 12.04.2026:**
|
||||
- **Infrastructure:** Docker-Services für `masterdata`, `events` und `zns-import` in `dc-backend.yaml` ergänzt.
|
||||
- **Gateway:** API-Gateway Routing für Masterdata (`/api/v1/masterdata`) und Events (`/api/v1/events`) konfiguriert.
|
||||
- **Frontend (Vereine):** `VereinRepository` (Ktor) und `VereinViewModel` implementiert für echtes Anlegen von Veranstaltern.
|
||||
- **Frontend (Events):** `TurnierViewModel` an das reale `TurnierRepository` angebunden.
|
||||
- **Fix:** `verein-feature` Abhängigkeiten korrigiert (Network/Ktor).
|
||||
- **Fix:** Polling-Endpoints im `ZnsImportViewModel` an das neue Gateway-Routing angepasst.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Phase 10.2 (Masterdata-Editoren & Organisation) - 12.04.2026:**
|
||||
- **Frontend:** `MasterdataEditDialogs.kt` für die Bearbeitung von Reiter- und Pferdedaten direkt im Turnier-Kontext.
|
||||
- **Frontend:** Erweiterung des `MasterdataRepository` um Schreibzugriffe (`saveReiter`, `savePferd`).
|
||||
- **Frontend:** Funktionale Suche für Turnierleiter im `Organisation`-Tab via `NennungViewModel` und Masterdata-API.
|
||||
- **Frontend:** State-Management für Stammdaten-Editoren im `NennungViewModel`.
|
||||
- **Fix:** Kompilierungsfehler in `ScreenPreviews.kt` behoben (fehlende Interface-Methoden in Mocks).
|
||||
- **Fix (Desktop Shell):** Fehlendes `turnierFeatureModule` in `main.kt` registriert und Login-Gate in `DesktopApp.kt` optimiert.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Phase 10 (Series-Context & Stammdaten) - 11.04.2026:**
|
||||
- **Frontend:** Stammdaten-Infrastruktur im `turnier-feature` (Repositories, DTOs, Domänenmodelle) für Reiter, Pferde, Funktionäre und Vereine.
|
||||
- **Frontend:** `NennungViewModel` zur Steuerung der Suche und Status-Verwaltung von Nennungen.
|
||||
- **Frontend:** Funktionalisierung des `Nennungen`-Tabs (Suche, Echt-Datenanbindung) und Vorbereitung des `Organisation`-Tabs.
|
||||
- **Frontend:** `DefaultMasterdataRepository` zur Suche in Reitern, Pferden und Funktionären via Backend-API.
|
||||
- **Netzwerk:** Erweiterung der `ApiRoutes` um Endpunkte für Masterdata und Nennungen.
|
||||
- **Phase 10 (Series-Context) Vorbereitung:**
|
||||
- **Frontend:** Neuer `SeriesScreen.kt` für die Verwaltung von Cups und Meisterschaften (konfigurierbare Reglements).
|
||||
- **Frontend:** Erweiterung des `AdminUebersichtScreen` (Cockpit) um KPI-Kacheln mit Direkt-Links zu Cups und Meisterschaften.
|
||||
- **Frontend:** Integration der Series-Navigation in die Breadcrumbs und das globale Routing (`Meisterschaften`, `Cups`).
|
||||
- **Turnier-Feature Hardening:**
|
||||
- **Frontend:** `STARTLISTEN` und `ERGEBNISLISTEN` Tabs vollständig an das `BewerbViewModel` angebunden (Bewerbs-Auswahl mit echten Daten).
|
||||
- **Frontend:** Implementierung der Starter-Anzeige in der Startliste (LazyColumn).
|
||||
|
||||
### Geändert
|
||||
- **Turnier-Feature:** Sichtbarkeit von `BewerbViewModel.generateStartliste()` auf `public` geändert, um den Aufruf aus dem Tab zu ermöglichen.
|
||||
- **Frontend (Desktop):** `ScreenPreviews.kt` aktualisiert zur Berücksichtigung der neuen ViewModel-Abhängigkeiten (`NennungViewModel`, `MasterdataRepository`).
|
||||
|
||||
### [Phase 9] - 11.04.2026
|
||||
- **Frontend:** Interaktiver Drag & Drop Zeitplan mit automatischem 5-Minuten-Snapping und Konflikt-Visualisierung.
|
||||
- **Frontend:** "B-Satz Export (ZNS)" Toolbar-Aktion mit integriertem Vorschau-Dialog.
|
||||
- **Frontend:** "Änderungs-Historie" (Audit-Log) Sektion zur Nachverfolgung von Zeitplan-Anpassungen.
|
||||
- **Backend:** `audit_log` Persistenz und Abfrage-API für manuelle Eingriffe in Bewerbe.
|
||||
- **Backend:** ZNS B-Satz Export Endpunkt (`/export/zns/b-satz`) zur Generierung von `BBEWERBE` Datensätzen.
|
||||
- **Core:** `FixedWidthLineBuilder` zur präzisen Generierung von ZNS-konformen Festbreiten-Formaten.
|
||||
|
||||
### Behoben
|
||||
- **Infrastruktur:** Veraltete `newSuspendedTransaction` in `DatabaseFactory.kt` durch moderne `suspendTransaction` (Exposed v1) ersetzt.
|
||||
- **Frontend (Desktop):** Kompilierfehler in `ScreenPreviews.kt` behoben, indem fehlende Interface-Methoden im Mock-Repository implementiert wurden.
|
||||
- **Backend (Tests):** `JdbcSQLSyntaxErrorException` im `BewerbeZeitplanIntegrationTest` durch Korrektur des Schema-Setups (Audit-Log Tabelle) gelöst.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Bugfix**: Behebung von Build-Fehlern im `veranstalter-feature` nach der Paket-Konsolidierung.
|
||||
- **Frontend**: `FakeVeranstalterRepository` in `commonMain` implementiert, um saubere KMP-DI zu ermöglichen.
|
||||
- **Frontend**: Veraltete Imports und Referenzen im `meldestelle-desktop` Shell und Previews korrigiert.
|
||||
- **Architektur:** Fachliches Konzept für Zeitplan-Optimierung (Drag & Drop) erstellt (`konzept-zeitplan-optimierung-de.md`).
|
||||
- **Architektur:** Spezifikation des Status-Automaten für Nennungen und Synchronisations-Logik (`status-automat-nennungen-de.md`).
|
||||
- **Rulebook:** Überprüfung und Spezifikation der Parcoursbesichtigung zu Pferd (§43 ÖTO) inkl. 5-Minuten-Puffer-Regel.
|
||||
- **Backend (Entries):** Erweiterung der Domain-Modelle `Bewerb` und `Abteilung` um Besichtigungs- und Pausen-Konfigurationen.
|
||||
- **Backend (Entries):** Neues Datenmodell `BesichtigungsBlock` für wettbewerbsübergreifende Parcoursbesichtigungen.
|
||||
- **Backend (Entries):** API-Endpunkt `PATCH /bewerbe/{id}/zeitplan` für schnelle Zeitplan-Updates implementiert.
|
||||
- **Backend (Entries):** `StartlistenService` um ÖTO-konforme Zeitberechnung (Besichtigungs-Puffer, Pausen-Intervalle) erweitert.
|
||||
|
||||
### Geändert
|
||||
- Masterdata/Domain: Umbenennungen zur Vereinheitlichung der Terminologie (DE):
|
||||
- `MasterdataLicenseRepository` → `LizenzRepository`
|
||||
- `LicenseMatrixService` → `LizenzMatrixService`
|
||||
- `LicenseMatrixServiceImpl` → `LizenzMatrixServiceImpl`
|
||||
- Test: `LicenseMatrixServiceTest` → `LiznezMatrixServiceTest` (exakt nach Vorgabe)
|
||||
- Infrastructure (Exposed): `LicenseTable` → `LizenzTable`
|
||||
- Docs: Begriff „reit_lizenzen“ → „reiterlizenzen“ in Glossar/UL konsolidiert.
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **Events-Service Bundle:** Vollständige Stabilisierung der `events` Services (Domain, Infrastructure, API, Service).
|
||||
- **Domain:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen (Kotlin 2.1.20+) und Harmonisierung mit dem Rulebook-Expert.
|
||||
- **Infrastructure:** Anpassung an den `org.jetbrains.exposed.v1` Namespace und Implementierung von UUID-Konvertierungen zwischen `kotlin.uuid.Uuid` (Domain) und `java.util.UUID` (DB).
|
||||
- **API:** Refactoring des `VeranstaltungController` zur direkten Repository-Nutzung (Alignment mit `entries` Service).
|
||||
- **Service-Config:** Umstellung auf Flyway-basiertes Tenant-Schema-Management in `EventsDatabaseConfiguration`.
|
||||
- **Build:** Behebung des `shadowJar` Fehlers in `events-infrastructure` durch Entfernen des unnötigen `ktor` Plugins in der Library-Schicht.
|
||||
|
||||
- Masterdata: Automatisches Seeding aller Reiterlizenzen (license_matrix) beim Start des `masterdata-service` via `ReiterlizenzenSeeder` (idempotent; SPRINGEN: LIZENZFREI,R1–R4; DRESSUR: LIZENZFREI,RD1–RD3).
|
||||
|
||||
- **ZNS-Import (LIZENZ01.dat):** Robuster Lizenz-Tokenizer und Normalizer implementiert.
|
||||
- Erkennung: `RD1..RD4`, `R1..R4`, `S1..S4`, `D2..D4`, Kombis `R{n}D{m}`, `R{n}S{k}`, `RDS4` (rechts-/letztes Vorkommen gewinnt).
|
||||
- Normalisierung: `S*→R*`, `D*→RD*`, `RD4→RD3` (bis Enum verfügbar), `R{n}S{k}→Rmax(n,k)`, `R{n}D{m}→R{n}+RD{m}`.
|
||||
- Integration: `ZnsReiterParser` füllt `lizenzen`-Liste (1:n) entsprechend und leitet `lizenzKlasse` bei fehlendem 4‑Spalten‑Code aus Token ab.
|
||||
- QA: Neue Unit-Tests (Tokenizer) für Beispiele `R2S3`, `R2D4`, `RD2` u. a.; alle Parser-Tests grün.
|
||||
|
||||
- **Core:** Modularisierte ZNS-Parser eingeführt (`ZnsVereinParser`, `ZnsReiterParser`, `ZnsPferdParser`, `ZnsFunktionaerParser`) zur Verbesserung der Wartbarkeit und Unterstützung von Einzelimporten.
|
||||
- **Fix:** SQL-Migrationsfehler in `V010` behoben, indem die Umbenennung der Spalte `name` in `verein_name` durch einen idempotenten `DO`-Block abgesichert wurde (behebt "Unable to resolve column 'name'").
|
||||
- **Infrastructure:** Datenbank-Migration `V010` hinzugefügt, um das Schema final mit den `Exposed`-Modellen zu synchronisieren.
|
||||
- **Infrastructure:** Datei-Archivierung für hochgeladene ZNS-ZIP-Dateien im `ZnsImportOrchestrator` implementiert.
|
||||
- **Infrastructure:** `ZnsImportService` vollständig auf die neuen spezialisierten Parser umgestellt und als Spring-Bean im Backend registriert.
|
||||
- **QA:** Umfassende Test-Suite `ZnsParserTest.kt` mit realen ZNS-Daten (Hämmerle, Neuwirth, etc.) erstellt; Korrektur der Extraktions-Logik für Mitgliedsnummern (Position 147) und Funktionär-Daten (RICHT01).
|
||||
- **QA:** Neue Betriebsanleitung für ZNS-Importer Tests erstellt: `docs/07_Infrastructure/runbooks/ZNS_Importer_Test_Manual.md`.
|
||||
- **Infrastructure:** `MasterdataDatabaseConfiguration` korrigiert: Expliziter Aufruf von `Database.connect()` hinzugefügt, um Abstürze beim Anwendungsstart ("No database specified") zu beheben.
|
||||
- **Infrastructure:** `application.yml` im `masterdata-service` vervollständigt (DataSource-Konfiguration mit `pg-user`/`pg-password` und Flyway-Aktivierung).
|
||||
- **Domain:** Legacy-Spezifikationen für ZNS-Schnittstellen (Import/Export) formalisiert:
|
||||
- `docs/03_Domain/02_Reference/Legacy_Specs/OETO-2026_Meldestelle_Pflichtenheft_V2.4.md` (Basis-Satzarten A-N)
|
||||
- `docs/03_Domain/02_Reference/Legacy_Specs/OETO-2026_Meldestelle_Erweiterung-Schnittstelle_2014.md` (XML-Erweiterung, LinkID-Logik)
|
||||
- **QA B-2:** `OnboardingValidator`-Objekt extrahiert; `OnboardingValidatorTest.kt` (17 Unit-Tests: Pflichtfeld-Guard,
|
||||
Doppelklick-Schutz, Abbrechen-Reset, rememberSaveable-Regression)
|
||||
- **QA B-3:** `AbteilungsRegelServiceTest.kt` um 14 Tests erweitert: CSN-C-NEU ≤95 cm / ≥100 cm Pflicht-Teilung,
|
||||
ORGANISATORISCH, SEPARATE_SIEGEREHRUNG, Caprilli-Regression, Grenzfälle 90/110 cm
|
||||
- **Domain:** `AbteilungsTeilungsTypE` um `ORGANISATORISCH` und `SEPARATE_SIEGEREHRUNG` erweitert
|
||||
|
||||
### Behoben
|
||||
|
||||
- **Masterdata/Infrastructure:** Kompilierfehler in `AltersklasseRepositoryImpl` durch Vereinheitlichung der Exposed-Tabellendefinition behoben:
|
||||
- `AltersklassenTable` → `AltersklasseTable`
|
||||
- Spalte `altersklassen_code` → `altersklasse_code`
|
||||
- Tabellenname `altersklassen` → `altersklasse`
|
||||
- **Masterdata/API:** Fehlendes Interface-Mapping ergänzt: `RegulationRepository` enthält nun `findAllTurnierklassen()`; `ExposedRegulationRepository` implementiert die Methode und `RegulationController` kompiliert wieder.
|
||||
- **ZNS-Import:** `AltersklassenExposedRepository` korrigiert (richtiger Domain-Typ `AltersklasseDefinition`, Mapping von `SparteE` und Zeitstempeln).
|
||||
|
||||
- **Migration V013:** Idempotent und robust gemacht. Alle `ALTER TABLE ... RENAME`-Operationen laufen nun nur, wenn die Quell-Tabelle existiert (Fix für "Unable to resolve table 'bundesland'/'turnierklasse'").
|
||||
- **Lizenz-Validierung:** `LicenseMatrixServiceImpl` um Cross-Discipline-Mapping R↔RD (ÖTO-Äquivalenzen) erweitert. Damit funktionieren Fälle wie Dressur-Starts mit Spring-Lizenz (R1→RD1, R2→RD2, R3/R4→RD3) bzw. umgekehrt konsistent.
|
||||
|
||||
- **Domain:** Striktere Spartenlizenz-Prüfung in `Reiter.hasLizenzForSparte` implementiert (RD1..RD3 nur DRESSUR; R1..R4 nur SPRINGEN). Behebt Testfehler „isEligible verweigert Start ohne passende Spartenlizenz“ im `LicenseMatrixServiceTest`.
|
||||
|
||||
### Behoben
|
||||
- **Backend (Entries):** Fehlschlagenden Unit-Test `berechneStartzeiten sollte Zeiten korrekt aufsummieren` korrigiert; der Test berücksichtigt nun den neuen 5-minütigen ÖTO-konformen Puffer nach der Parcoursbesichtigung (§43).
|
||||
- **Frontend (Desktop):** Build-Fehler ("No matching variant") beim `funktionaer-feature` behoben; fehlendes `build.gradle.kts` mit JVM-Target und Compose/Koin-Abhängigkeiten ergänzt.
|
||||
- **Frontend (Desktop):** Massive Inkonsistenzen in der Paketstruktur des `veranstalter-feature` bereinigt; alle Komponenten (ViewModel, Screens, Mocks) auf das Standardpaket `at.mocode.frontend.features.veranstalter` konsolidiert, um Redeklarationen und Import-Fehler zu beheben.
|
||||
- **Frontend (Desktop):** Kompilierfehler im `VeranstalterDetailScreen` durch korrekte Paket-Referenzierung des `FakeVeranstaltungStore` gelöst.
|
||||
|
||||
### Dokumentation
|
||||
- **Masterdata/Docs:** `REITER_LIZENZEN.md` überarbeitet:
|
||||
- Strikte Sparten-Trennung dokumentiert (RD1..RD3 nur Dressur; R1..R4 nur Springen).
|
||||
- Dressur-Tabelle korrigiert (R-Lizenzen entfernen; RD-Pflicht je Klasse).
|
||||
- Validierungslogik ergänzt (2-stufig: Spartenlizenz → Max-Turnierklasse; R↔RD Mapping nur zur Kappung, nicht zur Eligibility).
|
||||
- Vielseitigkeit (CCN/CCI) ergänzt: kumulative Anforderungen (Dressur RD* UND Springen R* je Klasse); Startkartenregel für Einsteiger.
|
||||
- Fahren (CAN/CAI) ergänzt: aktueller Systemzustand ohne `F*`‑Lizenzen dokumentiert; Teilnahme über Startkarte/Ausschreibung, geplante Enum‑Erweiterung vermerkt.
|
||||
- §15‑Tabelle (kompakt) integriert und auf ÖTO‑Referenz verlinkt; Bedeutungen „B,C“ und „LP“ erläutert. Hinweis aufgenommen, dass `RD4` derzeit nicht als Enum vorhanden ist und wie `RD3` behandelt wird.
|
||||
- Kombinationsreihen gemäß §15 ergänzt: `R1S2`, `R1S3`, `R1S4`, `R2S3`, `R2S4`, `R3S4` (neuer Unterabschnitt 2.6 mit Tabelle, identische Spalten wie 2.5).
|
||||
|
||||
### Behoben
|
||||
|
||||
- **Masterdata:** Qualifikations-Management für Funktionäre (Richter/Parcoursbauer) professionalisiert: Umstellung von unstrukturiertem Text auf offizielle ÖTO/FEI Master-Daten Referenzen (`QualifikationMasterTable`).
|
||||
- **Masterdata:** Fehlende Tabelle `funktionaer_qualifikation` in der Initialisierung beider Services (`masterdata` und `zns-import`) ergänzt, um `PSQLException` während des ZNS-Imports zu beheben.
|
||||
- **Infrastructure:** Start-Probleme des `masterdata-service` endgültig behoben: Port-Konflikt zwischen Spring Boot (Management/Actuator) und dem Gateway (8081) durch Umzug auf Port 8086 (gemäß Infrastruktur-Vorgaben) gelöst.
|
||||
- **Infrastructure:** Port-Konflikt im `masterdata-service` durch Trennung der Bind-Adressen (Spring: 127.0.0.1, Ktor: 0.0.0.0) und Bereinigung verwaister Prozesse stabilisiert.
|
||||
- **Core:** Veraltete `ZnsLegacyParsersTest.kt` entfernt; alle Tests sind nun in `ZnsParserTest.kt` konsolidiert.
|
||||
- **Domain:** Fehlschlagenden `LicenseMatrixServiceTest` behoben; fehlende `reiterLizenz`-Daten in Test-Reitern ergänzt und Fallback-Logik in `LicenseMatrixServiceImpl` für spartenübergreifende Lizenzen (z.B. Springlizenz für Dressur-Basis) stabilisiert.
|
||||
- **Infrastructure:** Fehlschlagenden `RegulationSeedVerificationTest` behoben; Testdaten an das neue Modell (`reiterLizenz` Feld) angepasst.
|
||||
- **Infrastructure:** Kompilierfehler 'Unresolved reference lizenzKlasse' in `ReiterExposedRepository` behoben; fehlendes Feld `lizenzKlasse` zu `ReiterTable` und Datenbank-Migration `V010` hinzugefügt.
|
||||
- **Onboarding:** `remember` → `rememberSaveable` für `geraetName`, `sharedKey`, `znsStatus` in `OnboardingScreen.kt` (
|
||||
Felder gingen bei Zurück-Navigation verloren)
|
||||
- **AbteilungsRegelService:** CSN-C-NEU Pflicht-Teilungslogik implementiert (≤95 cm: ohne/mit Lizenz; ≥100 cm: R1/R2+);
|
||||
`SparteE`-Import ergänzt
|
||||
|
||||
- Desktop-Packaging konfiguriert: `.deb` (Linux), `.msi` (Windows), `.dmg` (macOS)
|
||||
- Zentrale Versionsdatei `version.properties` (Single Source of Truth für SemVer)
|
||||
- Automatisches Git-Tagging via CI/CD (`release.yml` Gitea Actions Workflow)
|
||||
- `CHANGELOG.md` eingeführt (dieses Dokument)
|
||||
|
||||
---
|
||||
|
||||
## [1.0.6-SNAPSHOT] — 2026-04-10
|
||||
### [1.0.6-SNAPSHOT] — 2026-04-10
|
||||
|
||||
### Hinzugefügt
|
||||
- **Entries-Domain:** Strukturiertes Abteilungs-Warnungssystem gemäß ÖTO § 39 implementiert.
|
||||
|
||||
@@ -13,12 +13,12 @@ Die gesamte Projektdokumentation (Architektur, Fachdomäne, Entwickler-Anleitung
|
||||
|
||||
**Starte hier:** [**→ docs/README.md**](./docs/README.md)
|
||||
|
||||
| Bereich | Inhalt |
|
||||
|-----------------------------------------------|---------------------------------------------|
|
||||
| Bereich | Inhalt |
|
||||
|-----------------------------------------------|---------------------------------------------------|
|
||||
| [01_Architecture](./docs/01_Architecture) | Master Roadmap, ADRs, C4‑Modelle, Desktop‑Konzept |
|
||||
| [02_Guides](./docs/02_Guides) | Setup-Anleitungen, Entwickler-Guidelines |
|
||||
| [03_Domain](./docs/03_Domain) | Fachlichkeit, Turnierregeln, Entities |
|
||||
| [07_Infrastructure](./docs/07_Infrastructure) | Docker, Keycloak, CI/CD, Zora-Infrastruktur |
|
||||
| [02_Guides](./docs/02_Guides) | Setup-Anleitungen, Entwickler-Guidelines |
|
||||
| [03_Domain](./docs/03_Domain) | Fachlichkeit, Turnierregeln, Entities |
|
||||
| [07_Infrastructure](./docs/07_Infrastructure) | Docker, Keycloak, CI/CD, Zora-Infrastruktur |
|
||||
|
||||
Wesentliche Architektur-Referenz: [Offline‑First Desktop & Backend (Kurzkonzept)](./docs/01_Architecture/konzept-offline-first-desktop-backend-de.md)
|
||||
|
||||
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# =============================================================================
|
||||
# Conveyor Configuration for Meldestelle Desktop App
|
||||
# =============================================================================
|
||||
# Dieser Build-Weg ermöglicht das Cross-Packaging für Windows (MSI) auf Linux.
|
||||
# Dokumentation: https://conveyor.hydraulic.dev/
|
||||
# =============================================================================
|
||||
|
||||
include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
|
||||
|
||||
# Basis-Import der Gradle-Konfiguration (sofern das Plugin genutzt wird,
|
||||
# aber wir definieren es hier explizit für maximale Kontrolle im CI/CD).
|
||||
app {
|
||||
# Anzeige-Name und Vendor
|
||||
display-name = "Meldestelle"
|
||||
rdns-name = "at.mocode.meldestelle"
|
||||
vendor = "mo-code.at"
|
||||
contact-email = "support@mo-code.at"
|
||||
|
||||
# Version aus version.properties (Conveyor kann HOCON-Variablen nutzen)
|
||||
# Für diesen Task hart codiert oder via CLI-Flag --variable übergeben.
|
||||
version = "1.0.0"
|
||||
|
||||
# Beschreibung
|
||||
description = "ÖTO-konforme Turnier-Meldestelle – Desktop App"
|
||||
|
||||
# Ziel-Plattformen
|
||||
# Wir konzentrieren uns auf Windows, können aber Linux/Mac später ergänzen.
|
||||
site.base-url = "localhost" # Später echte Update-URL
|
||||
|
||||
# Icons
|
||||
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.png"
|
||||
|
||||
# Einbetten der JRE (Temurin 21 wie in CI genutzt)
|
||||
jvm {
|
||||
gui {
|
||||
main-class = "at.mocode.frontend.shell.desktop.MainKt"
|
||||
}
|
||||
|
||||
# JVM-Argumente (analog build.gradle.kts)
|
||||
jvm-options = [
|
||||
"-Xms128m",
|
||||
"-Xmx512m",
|
||||
"-Dfile.encoding=UTF-8"
|
||||
]
|
||||
}
|
||||
|
||||
# Input-Dateien: Hier ziehen wir die Uber-JAR oder die Gradle-Outputs.
|
||||
# Da wir plattformunabhängig bleiben wollen, nutzen wir das Gradle-Output-Dir.
|
||||
inputs += "frontend/shells/meldestelle-desktop/build/libs/meldestelle-desktop-jvm-*.jar"
|
||||
|
||||
# Windows-spezifische Einstellungen
|
||||
windows {
|
||||
# Icon als .ico
|
||||
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.ico"
|
||||
# GUID für Upgrades (muss stabil bleiben)
|
||||
upgrade-uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
# Menü-Eintrag
|
||||
menu-group = "Meldestelle"
|
||||
# Verknüpfung
|
||||
desktop-shortcut = true
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
+41
-41
@@ -1,48 +1,48 @@
|
||||
name: "${PROJECT_NAME:-meldestelle}"
|
||||
|
||||
services:
|
||||
# ==========================================
|
||||
# 3. FRONTEND (UI)
|
||||
# ==========================================
|
||||
# services:
|
||||
# # ==========================================
|
||||
# # 3. FRONTEND (UI)
|
||||
# # ==========================================
|
||||
|
||||
# --- 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
|
||||
+1
-1
@@ -3,7 +3,7 @@ name: "${PROJECT_NAME:-meldestelle}"
|
||||
include:
|
||||
- dc-infra.yaml
|
||||
- dc-backend.yaml
|
||||
- dc-gui.yaml
|
||||
#- dc-gui.yaml
|
||||
- dc-ops.yaml
|
||||
|
||||
# Globale Definitionen, falls nötig (meistens reichen die in den Teil-Dateien,
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-15
|
||||
---
|
||||
# Frontend-Architektur & Modularisierungsstrategie
|
||||
|
||||
**Status:** ENTWURF
|
||||
**Zuletzt aktualisiert:** 2026-01-19
|
||||
**Kontext:** Migration zu Clean Architecture & Feature-Modulen
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht
|
||||
Die Frontend-Architektur von **Meldestelle** basiert auf **Kotlin Multiplatform (KMP)** mit **Compose Multiplatform** für die Benutzeroberfläche. Wir folgen einem strikten **Clean Architecture**-Ansatz, um Testbarkeit, Skalierbarkeit und Trennung der Zuständigkeiten sicherzustellen.
|
||||
|
||||
## 2. Modulstruktur
|
||||
Das Projekt ist in folgende Schichten unterteilt:
|
||||
|
||||
### 2.1 Core-Module (`frontend/core`)
|
||||
Wiederverwendbare Komponenten, die unabhängig von spezifischen Geschäftsfunktionen sind.
|
||||
* `core-network`: Zentrale HTTP-Client-Konfiguration (Auth, Logging, ContentNegotiation).
|
||||
* `core-sync`: Generische Synchronisierungslogik (`SyncManager`, `SyncableRepository`).
|
||||
* `core-ui`: Gemeinsame UI-Komponenten und Design-System.
|
||||
|
||||
### 2.2 Feature-Module (`frontend/features`)
|
||||
Jede Geschäftsdomäne (z.B. `ping`, `auth`, `events`) liegt in ihrem eigenen Modul.
|
||||
Ein Feature-Modul MUSS die **Clean Architecture** Paketstruktur einhalten:
|
||||
|
||||
* `at.mocode.{feature}.feature.domain`
|
||||
* **Entitäten:** Reine Datenklassen.
|
||||
* **Interfaces:** Repository-Interfaces, Service-Interfaces.
|
||||
* **Use Cases:** Geschäftslogik (optional, für komplexe Logik).
|
||||
* `at.mocode.{feature}.feature.data`
|
||||
* **Implementierungen:** Repository-Implementierungen, API-Clients.
|
||||
* **DTOs:** Data Transfer Objects (wenn von Domain-Entitäten abweichend).
|
||||
* `at.mocode.{feature}.feature.presentation`
|
||||
* **ViewModels:** Zustandsverwaltung.
|
||||
* **Screens:** Composable-Funktionen.
|
||||
* `at.mocode.{feature}.feature.di`
|
||||
* **Koin-Modul:** Konfiguration der Dependency Injection.
|
||||
|
||||
### 2.3 Shells (`frontend/shells`)
|
||||
Anwendungs-Einstiegspunkte, die alles zusammenführen.
|
||||
* `meldestelle-portal`: Die Haupt-Web-/Desktop-Anwendung.
|
||||
|
||||
## 3. Migrationsstrategie (Übergangsphase)
|
||||
Wir migrieren aktuell von einer monolithischen `clients`-Paketstruktur zu modularen Feature-Modulen.
|
||||
|
||||
**Regeln für die Migration:**
|
||||
1. **Neue Features:** Müssen direkt in `frontend/features/{name}` unter Verwendung der Clean Architecture-Struktur implementiert werden.
|
||||
2. **Bestehende Features:** Werden schrittweise migriert.
|
||||
3. **Koexistenz:** Während des Übergangs ist Legacy-Code in `clients/` erlaubt, aber als veraltet markiert.
|
||||
4. **Dependency Injection:** Legacy-Code muss die neuen Koin-Module verwenden, sofern verfügbar.
|
||||
5. **Keine Ghost-Klassen:** Klassen nicht duplizieren. Wenn eine Klasse in ein Feature-Modul verschoben wird, muss die alte in `clients/` gelöscht werden.
|
||||
|
||||
## 4. Wichtige Entscheidungen (ADRs)
|
||||
|
||||
### ADR-001: Entkopplung der Sync-Logik
|
||||
* **Entscheidung:** ViewModels dürfen nicht direkt vom `SyncManager` abhängen.
|
||||
* **Begründung:** Um einfacheres Testen zu ermöglichen und die Komplexität des generischen Sync-Mechanismus zu verbergen.
|
||||
* **Umsetzung:** Ein Domain-Service-Interface (z.B. `PingSyncService`) einführen, das den `SyncManager`-Aufruf kapselt.
|
||||
|
||||
### ADR-002: Feature-Modul-Isolation
|
||||
* **Entscheidung:** Feature-Module sollten nach Möglichkeit nicht direkt voneinander abhängen.
|
||||
* **Kommunikation:** Gemeinsame Core-Module oder lose Kopplung über Interfaces/Events verwenden, wenn modulübergreifende Kommunikation nötig ist.
|
||||
|
||||
---
|
||||
|
||||
**Freigegeben von:** Lead Architect
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
---
|
||||
# Open-Source-Konformität & Lizenz-Checkliste
|
||||
|
||||
Dieses Dokument dient der Überwachung und Sicherstellung der Open-Source-Konformität des Projekts **Meldestelle**. Es wird vom Lead Architect gepflegt.
|
||||
|
||||
## Status der Kern-Komponenten (Stand: Januar 2026)
|
||||
|
||||
| Komponente | Lizenz | Status | Risiko | Maßnahme / Kommentar |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Kotlin / JVM** | Apache 2.0 / GPLv2+CE | ✅ OK | Sehr gering | Standard-Stack. |
|
||||
| **Spring Boot / Cloud** | Apache 2.0 | ✅ OK | Sehr gering | |
|
||||
| **PostgreSQL** | PostgreSQL (BSD-like) | ✅ OK | Sehr gering | |
|
||||
| **Redis** | **RSALv2 / SSPL** | ⚠️ KRITISCH | Hoch | **Umstieg auf Valkey (BSD) geplant.** |
|
||||
| **Consul** | **BSL 1.1** | ⚠️ BEOBACHTEN | Mittel | Lizenzänderung durch HashiCorp. Für interne Nutzung aktuell unkritisch. |
|
||||
| **Keycloak** | Apache 2.0 | ✅ OK | Gering | |
|
||||
| **SQLDelight** | Apache 2.0 | ✅ OK | Sehr gering | |
|
||||
| **Redisson** | Apache 2.0 (Core) | ✅ OK | Gering | Sicherstellen, dass keine PRO-Features genutzt werden. |
|
||||
|
||||
---
|
||||
|
||||
## Checkliste für neue Abhängigkeiten
|
||||
|
||||
Bevor eine neue Bibliothek oder Infrastruktur-Komponente hinzugefügt wird, muss sie folgende Kriterien erfüllen:
|
||||
|
||||
1. **Lizenz-Typ:**
|
||||
* Bevorzugt: Apache 2.0, MIT, BSD (3-Clause).
|
||||
* Akzeptabel: MPL 2.0.
|
||||
* Einzelfallprüfung: LGPL (nur als dynamische Bibliothek).
|
||||
* **Verboten:** AGPL, SSPL, RSAL, BSL (sofern nicht explizit vom Architect freigegeben).
|
||||
|
||||
2. **Community & Governance:**
|
||||
* Wird das Projekt von einer neutralen Foundation (Apache, CNCF, Linux Foundation) verwaltet?
|
||||
* Gibt es eine "Single Vendor" Abhängigkeit (Risiko einer plötzlichen Lizenzänderung)?
|
||||
|
||||
3. **Transitive Abhängigkeiten:**
|
||||
* Bringt die Bibliothek "versteckte" Copyleft-Lizenzen mit? (Check via Gradle License Plugin).
|
||||
|
||||
---
|
||||
|
||||
## TODOs & Strategische Entscheidungen
|
||||
|
||||
- [ ] **Migration Redis -> Valkey:** Umstellung der Docker-Images und Test der Kompatibilität.
|
||||
- [ ] **Consul Review:** Jährliche Prüfung der BSL-Auswirkungen auf unser Deployment-Modell.
|
||||
- [ ] **Automatisierung:** Integration eines License-Check-Plugins in den CI-Build (z.B. `com.github.jk1.dependency-license-report`).
|
||||
|
||||
---
|
||||
*Dieses Dokument ist Teil der "Docs-as-Code" Strategie und muss bei jeder Änderung am Tech-Stack aktualisiert werden.*
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
---
|
||||
|
||||
# Open-Source-Konformität & Lizenz-Checkliste
|
||||
|
||||
Dieses Dokument dient der Überwachung und Sicherstellung der Open-Source-Konformität des Projekts **Meldestelle**. Es
|
||||
wird vom Lead Architect gepflegt.
|
||||
|
||||
## Status der Kern-Komponenten (Stand: Januar 2026)
|
||||
|
||||
| Komponente | Lizenz | Status | Risiko | Maßnahme / Kommentar |
|
||||
|:------------------------|:----------------------|:--------------|:------------|:------------------------------------------------------------------------|
|
||||
| **Kotlin / JVM** | Apache 2.0 / GPLv2+CE | ✅ OK | Sehr gering | Standard-Stack. |
|
||||
| **Spring Boot / Cloud** | Apache 2.0 | ✅ OK | Sehr gering | |
|
||||
| **PostgreSQL** | PostgreSQL (BSD-like) | ✅ OK | Sehr gering | |
|
||||
| **Redis** | **RSALv2 / SSPL** | ⚠️ KRITISCH | Hoch | **Umstieg auf Valkey (BSD) geplant.** |
|
||||
| **Consul** | **BSL 1.1** | ⚠️ BEOBACHTEN | Mittel | Lizenzänderung durch HashiCorp. Für interne Nutzung aktuell unkritisch. |
|
||||
| **Keycloak** | Apache 2.0 | ✅ OK | Gering | |
|
||||
| **SQLDelight** | Apache 2.0 | ✅ OK | Sehr gering | |
|
||||
| **Redisson** | Apache 2.0 (Core) | ✅ OK | Gering | Sicherstellen, dass keine PRO-Features genutzt werden. |
|
||||
|
||||
---
|
||||
|
||||
## Checkliste für neue Abhängigkeiten
|
||||
|
||||
Bevor eine neue Bibliothek oder Infrastruktur-Komponente hinzugefügt wird, muss sie folgende Kriterien erfüllen:
|
||||
|
||||
1. **Lizenz-Typ:**
|
||||
* Bevorzugt: Apache 2.0, MIT, BSD (3-Clause).
|
||||
* Akzeptabel: MPL 2.0.
|
||||
* Einzelfallprüfung: LGPL (nur als dynamische Bibliothek).
|
||||
* **Verboten:** AGPL, SSPL, RSAL, BSL (sofern nicht explizit vom Architect freigegeben).
|
||||
|
||||
2. **Community & Governance:**
|
||||
* Wird das Projekt von einer neutralen Foundation (Apache, CNCF, Linux Foundation) verwaltet?
|
||||
* Gibt es eine "Single Vendor" Abhängigkeit (Risiko einer plötzlichen Lizenzänderung)?
|
||||
|
||||
3. **Transitive Abhängigkeiten:**
|
||||
* Bringt die Bibliothek "versteckte" Copyleft-Lizenzen mit? (Check via Gradle License Plugin).
|
||||
|
||||
---
|
||||
|
||||
## TODOs & Strategische Entscheidungen
|
||||
|
||||
- [ ] **Migration Redis -> Valkey:** Umstellung der Docker-Images und Test der Kompatibilität.
|
||||
- [ ] **Consul Review:** Jährliche Prüfung der BSL-Auswirkungen auf unser Deployment-Modell.
|
||||
- [ ] **Automatisierung:** Integration eines License-Check-Plugins in den CI-Build (z.B.
|
||||
`com.github.jk1.dependency-license-report`).
|
||||
|
||||
---
|
||||
*Dieses Dokument ist Teil der "Docs-as-Code" Strategie und muss bei jeder Änderung am Tech-Stack aktualisiert werden.*
|
||||
+6
-1
@@ -3,6 +3,7 @@
|
||||
Status: April 2026
|
||||
|
||||
## ✅ Abgeschlossene Migrationen (Feature-Module)
|
||||
|
||||
- `billing-feature`: `at.mocode.frontend.features.billing` (KMP)
|
||||
- `verein-feature`: `at.mocode.frontend.features.verein` (KMP)
|
||||
- `nennung-feature`: `at.mocode.frontend.features.nennung` (KMP)
|
||||
@@ -13,11 +14,14 @@ Status: April 2026
|
||||
- `ping-feature`: `at.mocode.ping.feature` (muss noch auf `at.mocode.frontend.features.ping` vereinheitlicht werden)
|
||||
|
||||
## 🚧 Ausstehende Migrationen (von `at.mocode.desktop.v2` zu Features)
|
||||
Die folgenden Komponenten in `meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/` basieren noch auf `StoreV2` (In-Memory Mock) und sollten in KMP-Module überführt werden:
|
||||
|
||||
Die folgenden Komponenten in `meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/` basieren noch auf
|
||||
`StoreV2` (In-Memory Mock) und sollten in KMP-Module überführt werden:
|
||||
|
||||
1. **Onboarding**: `OnboardingScreen.kt` -> Design-System Integration erfolgt, KMP-Modul folgt.
|
||||
|
||||
## 🧹 Architektur-Cleanup
|
||||
|
||||
- [ ] `at.mocode.desktop.v2.StoreV2` entfernen, sobald alle Screens auf ViewModels und API-Repositories umgestellt sind.
|
||||
- [ ] `at.mocode.desktop.v2.TurnierStoreV2` konsolidieren mit dem `turnier-feature`.
|
||||
- [ ] Paketnamen vereinheitlichen: `at.mocode.ping.feature` -> `at.mocode.frontend.features.ping`.
|
||||
@@ -26,6 +30,7 @@ Die folgenden Komponenten in `meldestelle-desktop/src/jvmMain/kotlin/at/mocode/d
|
||||
- [ ] `DesktopMainLayout.kt`: Die `when`-Zweige für `v2` Screens aufräumen, sobald die Module bereit sind.
|
||||
|
||||
## ✅ Abgeschlossen am 11.04.2026
|
||||
|
||||
- Migration `pferde-feature`, `reiter-feature`, `funktionaer-feature`, `veranstalter-feature`.
|
||||
- Integration in `DesktopMainLayout` und `AppScreen`.
|
||||
- Bereinigung der Repository-Pakete.
|
||||
+50
-27
@@ -9,7 +9,9 @@ last_update: 2026-04-03
|
||||
|
||||
## Ziel und Rahmen
|
||||
|
||||
Dieses Dokument definiert das Synchronisations-Konzept zwischen der Compose Desktop App (Meldestelle-Zentrale) und dem Backend in einem Offline-First Szenario. Es baut auf ADR-0021 (Tenant-Isolation) und ADR-0022 (LAN-Sync mit Lamport-Uhren) auf und erweitert sie um die WAN/Backend-Synchronisation.
|
||||
Dieses Dokument definiert das Synchronisations-Konzept zwischen der Compose Desktop App (Meldestelle-Zentrale) und dem
|
||||
Backend in einem Offline-First Szenario. Es baut auf ADR-0021 (Tenant-Isolation) und ADR-0022 (LAN-Sync mit
|
||||
Lamport-Uhren) auf und erweitert sie um die WAN/Backend-Synchronisation.
|
||||
|
||||
Nicht-Ziele: Cloud-Realtime für Endnutzer, kollaboratives Editieren außerhalb des Veranstaltungsbetriebs.
|
||||
|
||||
@@ -18,32 +20,38 @@ Nicht-Ziele: Cloud-Realtime für Endnutzer, kollaboratives Editieren außerhalb
|
||||
1. Offline-First: Die Desktop-App ist voll funktionsfähig ohne Netzwerk; Synchronisation erfolgt opportunistisch.
|
||||
2. Event-isoliert: Pro Veranstaltung eigener Datenraum (gemäß ADR-0021). Keine Vermischung von Events.
|
||||
3. Einheitliches Änderungsmodell: Wiederverwendung des `SyncEvent`-Logs (ADR-0022) für Desktop↔Backend.
|
||||
4. Domänen-Mastership: Klare Schreibhoheiten reduzieren Konflikte, fachliche Regeln haben Vorrang vor rein technischen Timestamps.
|
||||
4. Domänen-Mastership: Klare Schreibhoheiten reduzieren Konflikte, fachliche Regeln haben Vorrang vor rein technischen
|
||||
Timestamps.
|
||||
5. Deterministische Konfliktauflösung: Lamport-Uhren + Regel-Matrix; keine Abhängigkeit von Systemuhren.
|
||||
|
||||
## Topologie & Rollen
|
||||
|
||||
- Backend (Zentrale Plattform):
|
||||
- Master für: Stammdaten (Reiter, Pferde, Vereine, Funktionäre), Identität/Rollen, Gebührenkataloge, globale Referenzen.
|
||||
- Master für: Stammdaten (Reiter, Pferde, Vereine, Funktionäre), Identität/Rollen, Gebührenkataloge, globale
|
||||
Referenzen.
|
||||
- Aggregations-/Archiv-Quelle nach Veranstaltungsende (finale Ergebnisse, Abrechnungen).
|
||||
- Desktop (Meldestelle-Zentrale):
|
||||
- Master während der Veranstaltung für: Nennungen (operativ), Startreihenfolgen, Startlisten-Status, Ergebnisse/Protokolle (falls Richter nicht direkt am Backend), Kassa-Operationen vor Ort.
|
||||
- Hält lokales `SyncEvent`-Log + Snapshots (vgl. ADR-0022) und synchronisiert mit Backend, sobald Konnektivität besteht.
|
||||
- Master während der Veranstaltung für: Nennungen (operativ), Startreihenfolgen, Startlisten-Status,
|
||||
Ergebnisse/Protokolle (falls Richter nicht direkt am Backend), Kassa-Operationen vor Ort.
|
||||
- Hält lokales `SyncEvent`-Log + Snapshots (vgl. ADR-0022) und synchronisiert mit Backend, sobald Konnektivität
|
||||
besteht.
|
||||
|
||||
Hinweis Mehrfach-Desktops: Genau eine „Zentrale“ pro Veranstaltung besitzt Schreibhoheit (Konfig-Flag `isEventAuthority=true`). Weitere Geräte sind Replikate/Clients.
|
||||
Hinweis Mehrfach-Desktops: Genau eine „Zentrale“ pro Veranstaltung besitzt Schreibhoheit (Konfig-Flag
|
||||
`isEventAuthority=true`). Weitere Geräte sind Replikate/Clients.
|
||||
|
||||
## Datenkategorien & Mastership
|
||||
|
||||
| Kategorie | Master | Desktop Rechte | Backend Rechte |
|
||||
|--------------------------|----------------|--------------------------------|-----------------------------------|
|
||||
| Stammdaten (Actor) | Backend | Lesen, lokal „provisional“ anlegen (Temp-ID) | Vollzugriff, ID-Zuteilung, Merge |
|
||||
| Veranstaltungs-Stammdaten| Backend | Lesen | Vollzugriff |
|
||||
| Nennungen operativ | Desktop | Vollzugriff | Lesen, Import nach Sync |
|
||||
| Startreihenfolge/Status | Desktop | Vollzugriff | Lesen, Import nach Sync |
|
||||
| Bewertungen/Ergebnisse | Desktop/Richter| Vollzugriff (Eventzeitraum) | Lesen, Publikation/Archiv |
|
||||
| Kassa/Finanzen vor Ort | Desktop | Vollzugriff | Lesen, Abgleich Summen |
|
||||
| Kategorie | Master | Desktop Rechte | Backend Rechte |
|
||||
|---------------------------|-----------------|----------------------------------------------|----------------------------------|
|
||||
| Stammdaten (Actor) | Backend | Lesen, lokal „provisional“ anlegen (Temp-ID) | Vollzugriff, ID-Zuteilung, Merge |
|
||||
| Veranstaltungs-Stammdaten | Backend | Lesen | Vollzugriff |
|
||||
| Nennungen operativ | Desktop | Vollzugriff | Lesen, Import nach Sync |
|
||||
| Startreihenfolge/Status | Desktop | Vollzugriff | Lesen, Import nach Sync |
|
||||
| Bewertungen/Ergebnisse | Desktop/Richter | Vollzugriff (Eventzeitraum) | Lesen, Publikation/Archiv |
|
||||
| Kassa/Finanzen vor Ort | Desktop | Vollzugriff | Lesen, Abgleich Summen |
|
||||
|
||||
Konflikte über Kategoriegrenzen werden durch Mastership-Regeln verhindert; verbleibende Konflikte werden per Regel-Matrix gelöst.
|
||||
Konflikte über Kategoriegrenzen werden durch Mastership-Regeln verhindert; verbleibende Konflikte werden per
|
||||
Regel-Matrix gelöst.
|
||||
|
||||
## Änderungsmodell (Wiederverwendung `SyncEvent`)
|
||||
|
||||
@@ -71,11 +79,13 @@ data class SyncEvent(
|
||||
## Lamport-Uhren & Vector-Clock (Optional)
|
||||
|
||||
- Primär: Lamport-Uhren wie ADR-0022. Gleichstand → lexikografisch größere `originNodeId` gewinnt (Determinismus).
|
||||
- Optional für feingranulare Erkennung: Per-Aggregat Vector-Clock (`Map<nodeId, lamport>`) zur Diagnose; Entscheidungsgrundlage bleibt Lamport + Fachregeln.
|
||||
- Optional für feingranulare Erkennung: Per-Aggregat Vector-Clock (`Map<nodeId, lamport>`) zur Diagnose;
|
||||
Entscheidungsgrundlage bleibt Lamport + Fachregeln.
|
||||
|
||||
## Sync-Protokoll Desktop ↔ Backend (HTTPS)
|
||||
|
||||
Transport: HTTPS (HTTP/2), JSON oder Protobuf, idempotente Endpunkte. Auth: mTLS zwischen Desktop und Backend ODER OAuth2 Client Credentials + Signatur der Batch-Payload.
|
||||
Transport: HTTPS (HTTP/2), JSON oder Protobuf, idempotente Endpunkte. Auth: mTLS zwischen Desktop und Backend ODER
|
||||
OAuth2 Client Credentials + Signatur der Batch-Payload.
|
||||
|
||||
Empfohlene Endpunkte (pro `eventId`):
|
||||
|
||||
@@ -94,42 +104,55 @@ Idempotenz: Jeder `SyncEvent` wird durch `(eventId, originNodeId, sequenceNumber
|
||||
## Konfliktauflösung
|
||||
|
||||
1) Strukturkonflikte (gleiches Aggregat, konkurrierende Events):
|
||||
- Wenn eine Seite nicht Master ist → Event wird angenommen, aber als `PENDING_REVIEW` markiert; fachliche Entscheidung erforderlich (Backend-UI oder Desktop-Review-Queue).
|
||||
|
||||
- Wenn eine Seite nicht Master ist → Event wird angenommen, aber als `PENDING_REVIEW` markiert; fachliche Entscheidung
|
||||
erforderlich (Backend-UI oder Desktop-Review-Queue).
|
||||
- Beide Master (Sonderfälle, z. B. Ergebnisse während parallelem Backend-Fix):
|
||||
- Lamport höher gewinnt.
|
||||
- Gleichstand → `originNodeId`-Tiebreaker.
|
||||
- Zusätzlich fachliche Heuristik optional: „Korrektur-Events“ (z. B. `ErgebnisKorrigiert`) schlagen normale `ErgebnisErfasst` bei gleichem Lamport.
|
||||
- Zusätzlich fachliche Heuristik optional: „Korrektur-Events“ (z. B. `ErgebnisKorrigiert`) schlagen normale
|
||||
`ErgebnisErfasst` bei gleichem Lamport.
|
||||
|
||||
2) Identitätskonflikte (provisionale Stammdaten):
|
||||
|
||||
- Desktop darf temporäre Einträge (Temp-ID `tmp-...`) erzeugen.
|
||||
- Beim Push führt Backend `Upsert+Merge` aus, weist finale IDs zu und liefert `IdMapping { tmpId -> finalId }` zurück.
|
||||
- Desktop ersetzt Referenzen transaktional und emittiert ein lokales `IdRemapped`-Event (kein Re-Upload nötig, außer für Diagnose).
|
||||
- Desktop ersetzt Referenzen transaktional und emittiert ein lokales `IdRemapped`-Event (kein Re-Upload nötig, außer für
|
||||
Diagnose).
|
||||
|
||||
3) Reihenfolge-/Kausalitätskonflikte:
|
||||
- Bei fehlenden Vorgänger-Events antwortet Backend mit `rejected: [id]` und `requiredSinceSeq`. Desktop zieht Delta (`pull`) und wiederholt den `push`.
|
||||
|
||||
- Bei fehlenden Vorgänger-Events antwortet Backend mit `rejected: [id]` und `requiredSinceSeq`. Desktop zieht Delta (
|
||||
`pull`) und wiederholt den `push`.
|
||||
|
||||
## Snapshots & Recovery
|
||||
|
||||
- Snapshot-Intervall: standardmäßig 100 Events pro `(aggregateType, scope)` (wie ADR-0022), für WAN-Sync zusätzlich Full-State-Snapshot pro Veranstaltung vor Event-Abschluss.
|
||||
- Recovery: Desktop kann mit leerem Log starten → `snapshot/request` → Full-State + `snapshotSeq` → weitere Deltas über `pull`.
|
||||
- USB-Fallback (Notbetrieb): Export/Import von `sync_events` und `sync_snapshots` als verschlüsselte Archive (`.msync`). Offene Spezifikation; separater PoC.
|
||||
- Snapshot-Intervall: standardmäßig 100 Events pro `(aggregateType, scope)` (wie ADR-0022), für WAN-Sync zusätzlich
|
||||
Full-State-Snapshot pro Veranstaltung vor Event-Abschluss.
|
||||
- Recovery: Desktop kann mit leerem Log starten → `snapshot/request` → Full-State + `snapshotSeq` → weitere Deltas über
|
||||
`pull`.
|
||||
- USB-Fallback (Notbetrieb): Export/Import von `sync_events` und `sync_snapshots` als verschlüsselte Archive (`.msync`).
|
||||
Offene Spezifikation; separater PoC.
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- Mandantentrennung: Jeder Request trägt `X-Event-Id` (ADR-0021). Backend validiert gegen `control.tenants`.
|
||||
- Transport: `https` + mTLS (bevorzugt) oder `https` + OAuth2 Client Credentials. Payload-Signatur (HMAC-SHA256) empfohlen.
|
||||
- Transport: `https` + mTLS (bevorzugt) oder `https` + OAuth2 Client Credentials. Payload-Signatur (HMAC-SHA256)
|
||||
empfohlen.
|
||||
- Integrität: `checksum` pro Event wird serverseitig geprüft; Mismatch → Reject.
|
||||
- Rechte: Backend erzwingt Mastership-Regeln serverseitig; Verstöße → `PENDING_REVIEW` + Audit-Log.
|
||||
|
||||
## Fehlerfälle & Resilienz
|
||||
|
||||
- Netzwerkfehler: Exponentielles Backoff (bis 5 min), Offline-Queue unbegrenzt (bounded by disk quota), Telemetrie im UI.
|
||||
- Netzwerkfehler: Exponentielles Backoff (bis 5 min), Offline-Queue unbegrenzt (bounded by disk quota), Telemetrie im
|
||||
UI.
|
||||
- Schema-Divergenz: `minSupportedSchema` aus `hello`; Desktop migriert vor weiterem Sync oder schaltet in Read-Only.
|
||||
- Duplikate: Idempotenz-Keys verhindern Doppelverarbeitung. ACK enthält höchste verarbeitete `sequenceNumber`.
|
||||
|
||||
## Observability
|
||||
|
||||
- Metriken: `sync_push_events_total`, `sync_pull_events_total`, `sync_rejected_total`, `sync_latency_ms` (p50/p95), `offline_duration_s`.
|
||||
- Metriken: `sync_push_events_total`, `sync_pull_events_total`, `sync_rejected_total`, `sync_latency_ms` (p50/p95),
|
||||
`offline_duration_s`.
|
||||
- Logs: pro Event `tenant`, `originNodeId`, `seq`, `aggType`, `eventType`, `result`.
|
||||
- UI: Status-Anzeige (Verbunden, Getrennt, Ausstehend X), Konflikt-Review-Queue.
|
||||
|
||||
+52
-28
@@ -5,72 +5,96 @@
|
||||
> **Kontext:** Phase 9 — Zeitplan & Protokollierung
|
||||
|
||||
## 1. Vision & Zielsetzung
|
||||
Die Zeitplan-Optimierung ist das zentrale Werkzeug für die Meldestelle, um den Turnierablauf dynamisch an die Gegebenheiten vor Ort anzupassen. Ziel ist eine intuitive, visuelle Oberfläche (Kalender-Ansicht), in der Bewerbe und Abteilungen per Drag & Drop verschoben werden können, wobei das System automatisch Auswirkungen auf Folge-Bewerbe berechnet und vor Konflikten warnt.
|
||||
|
||||
Die Zeitplan-Optimierung ist das zentrale Werkzeug für die Meldestelle, um den Turnierablauf dynamisch an die
|
||||
Gegebenheiten vor Ort anzupassen. Ziel ist eine intuitive, visuelle Oberfläche (Kalender-Ansicht), in der Bewerbe und
|
||||
Abteilungen per Drag & Drop verschoben werden können, wobei das System automatisch Auswirkungen auf Folge-Bewerbe
|
||||
berechnet und vor Konflikten warnt.
|
||||
|
||||
## 2. Fachliche Anforderungen (Use Cases)
|
||||
|
||||
### UC-1: Verschieben eines Bewerbs/einer Abteilung
|
||||
|
||||
- Ein Benutzer zieht einen Bewerb oder eine Abteilung auf eine neue Startzeit oder einen anderen Austragungsplatz.
|
||||
- **System-Reaktion:**
|
||||
- Neuberechnung der Endzeit basierend auf `reitdauerMinuten * starterAnzahl + besichtigungMinuten + umbauMinuten`.
|
||||
- Prüfung auf Überschneidungen am selben Austragungsplatz.
|
||||
- Warnung bei Konflikten (z.B. Richter-Doppelbelegung).
|
||||
- **System-Reaktion:**
|
||||
- Neuberechnung der Endzeit basierend auf `reitdauerMinuten * starterAnzahl + besichtigungMinuten + umbauMinuten`.
|
||||
- Prüfung auf Überschneidungen am selben Austragungsplatz.
|
||||
- Warnung bei Konflikten (z.B. Richter-Doppelbelegung).
|
||||
|
||||
### UC-2: Dynamische Zeitplan-Anpassung („Anschließend“)
|
||||
|
||||
- Bewerbe können als `ANSCHLIESSEND` markiert werden (`beginnZeitTyp`).
|
||||
- **System-Reaktion:** Wenn der vorangehende Bewerb verschoben wird oder länger dauert, verschiebt sich der anschließende Bewerb automatisch mit.
|
||||
- **System-Reaktion:** Wenn der vorangehende Bewerb verschoben wird oder länger dauert, verschiebt sich der
|
||||
anschließende Bewerb automatisch mit.
|
||||
|
||||
### UC-3: Drag & Drop in der Startliste
|
||||
|
||||
- Innerhalb einer Startliste können Teilnehmer per Drag & Drop umsortiert werden.
|
||||
- **System-Reaktion:** Automatische Aktualisierung der Kopfnummern (Startnummern) und Neuberechnung der individuellen Startzeiten.
|
||||
- **System-Reaktion:** Automatische Aktualisierung der Kopfnummern (Startnummern) und Neuberechnung der individuellen
|
||||
Startzeiten.
|
||||
|
||||
### UC-4: Protokollierung (Audit Log)
|
||||
|
||||
- Jede manuelle Änderung am Zeitplan oder der Startlisten-Reihenfolge muss protokolliert werden.
|
||||
- **Grund:** Nachvollziehbarkeit bei Einsprüchen und Synchronisation zwischen verschiedenen Arbeitsplätzen (Meldestelle ↔ Richterturm).
|
||||
- **Grund:** Nachvollziehbarkeit bei Einsprüchen und Synchronisation zwischen verschiedenen Arbeitsplätzen (
|
||||
Meldestelle ↔ Richterturm).
|
||||
|
||||
## 3. Datenmodell-Erweiterungen & Logik
|
||||
|
||||
### 3.1 Erweiterung `Bewerb` & `Abteilung`
|
||||
|
||||
Das bestehende Modell in `entries-domain` deckt bereits viele Felder ab. Für die Optimierung präzisieren wir:
|
||||
|
||||
- **`Umbauzeit`:** Zeit zwischen zwei Abteilungen oder Bewerben.
|
||||
- **`Pausen`:** Geplante Unterbrechungen (z.B. Mittagspause, Platzpflege) werden als spezielle „Blocker-Events“ im Zeitplan geführt.
|
||||
- **`Pausen`:** Geplante Unterbrechungen (z.B. Mittagspause, Platzpflege) werden als spezielle „Blocker-Events“ im
|
||||
Zeitplan geführt.
|
||||
- **`BesichtigungsBlock` (NEU):** Eigenständiges Objekt für Parcoursbesichtigungen.
|
||||
- Kann mit mehreren Bewerben/Abteilungen verknüpft werden (Cross-Competition Inspection).
|
||||
- Unterstützt Typen: `ZU_FUSS` (Standard) und `ZU_PFERD` (Spezialfall für Springpferde bis 110cm gemäß ÖTO).
|
||||
- Validierung: Mindestens 5 Min. Puffer vor dem ersten Start (ÖTO §43).
|
||||
- Kann mit mehreren Bewerben/Abteilungen verknüpft werden (Cross-Competition Inspection).
|
||||
- Unterstützt Typen: `ZU_FUSS` (Standard) und `ZU_PFERD` (Spezialfall für Springpferde bis 110cm gemäß ÖTO).
|
||||
- Validierung: Mindestens 5 Min. Puffer vor dem ersten Start (ÖTO §43).
|
||||
|
||||
### 3.2 Zeitberechnungs-Algorithmus (Präzisierung)
|
||||
|
||||
Die Logik in `StartlistenService.berechneStartzeiten` wird erweitert:
|
||||
|
||||
1. **Basis:** `Startzeit` der Abteilung.
|
||||
2. **Vorlauf:** `besichtigungMinuten`.
|
||||
3. **Starter-Loop:**
|
||||
- `individuelleStartzeit = aktuelleZeit`.
|
||||
- `aktuelleZeit += bewerb.reitdauerMinuten`.
|
||||
- *Neu:* Berücksichtigung von festen Pausen nach X Startern (z.B. 10 Min. Pause alle 20 Starter).
|
||||
3. **Starter-Loop:**
|
||||
- `individuelleStartzeit = aktuelleZeit`.
|
||||
- `aktuelleZeit += bewerb.reitdauerMinuten`.
|
||||
- *Neu:* Berücksichtigung von festen Pausen nach X Startern (z.B. 10 Min. Pause alle 20 Starter).
|
||||
4. **Nachlauf:** `umbauMinuten` am Ende der Abteilung/des Bewerbs.
|
||||
|
||||
### 3.3 Drag & Drop Logik (Frontend & Backend)
|
||||
- **Frontend (Compose Desktop):**
|
||||
- Implementierung eines `DraggableBewerbItem`.
|
||||
- Visuelle Darstellung von Konflikten (Rote Markierung bei Überlappung).
|
||||
|
||||
- **Frontend (Compose Desktop):**
|
||||
- Implementierung eines `DraggableBewerbItem`.
|
||||
- Visuelle Darstellung von Konflikten (Rote Markierung bei Überlappung).
|
||||
- **Backend (API):**
|
||||
- Neuer Endpunkt `PATCH /bewerbe/{id}/zeitplan` für schnelle Updates.
|
||||
- Validierung der neuen Zeiten gegen den `austragungsplatzId` und `richterEinsaetze`.
|
||||
- Neuer Endpunkt `PATCH /bewerbe/{id}/zeitplan` für schnelle Updates.
|
||||
- Validierung der neuen Zeiten gegen den `austragungsplatzId` und `richterEinsaetze`.
|
||||
|
||||
## 4. Konflikt-Management
|
||||
|
||||
Das System arbeitet nach dem Prinzip **„Warnen statt Blockieren“** (ADR-0016):
|
||||
|
||||
- **Harte Fehler:** Nur bei technischer Unmöglichkeit (z.B. Datum in der Vergangenheit bei Live-Betrieb).
|
||||
- **Warnungen:**
|
||||
- Überlappung auf dem Platz.
|
||||
- Richter hat gleichzeitig Einsatz in anderem Bewerb.
|
||||
- Zeitunterschreitung für Reiter (Reiter startet in zwei kurz aufeinanderfolgenden Bewerben).
|
||||
- **Warnungen:**
|
||||
- Überlappung auf dem Platz.
|
||||
- Richter hat gleichzeitig Einsatz in anderem Bewerb.
|
||||
- Zeitunterschreitung für Reiter (Reiter startet in zwei kurz aufeinanderfolgenden Bewerben).
|
||||
|
||||
## 5. Synchronisation (Offline-First)
|
||||
Änderungen am Zeitplan erzeugen `SyncEvents` (gemäß ADR-0022).
|
||||
- **Lamport-Uhren:** Stellen sicher, dass bei gleichzeitigen Änderungen an zwei Laptops die zeitlich spätere Änderung (oder die mit höherer Priorität) gewinnt.
|
||||
- **Echtzeit-Update:** Über WebSockets werden Zeitplan-Änderungen sofort an alle verbundenen Clients (z.B. Anzeige-Monitor, Richter-Tablet) gepusht.
|
||||
|
||||
Änderungen am Zeitplan erzeugen `SyncEvents` (gemäß ADR-0022).
|
||||
|
||||
- **Lamport-Uhren:** Stellen sicher, dass bei gleichzeitigen Änderungen an zwei Laptops die zeitlich spätere Änderung (
|
||||
oder die mit höherer Priorität) gewinnt.
|
||||
- **Echtzeit-Update:** Über WebSockets werden Zeitplan-Änderungen sofort an alle verbundenen Clients (z.B.
|
||||
Anzeige-Monitor, Richter-Tablet) gepusht.
|
||||
|
||||
## 6. Nächste Schritte
|
||||
|
||||
1. **Backend:** ✅ Implementiert (BewerbService, API, Zeitberechnungs-Pausen).
|
||||
2. **Frontend:** Prototyping der Kalender-Ansicht mit Compose Desktop.
|
||||
3. **QA:** Test-Szenarien für komplexe Verschiebungen (Kettenreaktionen bei „Anschließend“).
|
||||
+31
-10
@@ -5,42 +5,63 @@
|
||||
> **Kontext:** Zeitplan-Optimierung & Praxis-Szenarien
|
||||
|
||||
## 1. Parcoursbesichtigung zu Pferd
|
||||
|
||||
Die Aussage des Users wurde geprüft und bestätigt.
|
||||
|
||||
### 1.1 Regelwerks-Referenz (ÖTO 2026)
|
||||
|
||||
Gemäß **ÖTO Teil B, § 43 (Abweichungen)** gilt:
|
||||
|
||||
- **Normalfall:** Parcoursbesichtigung erfolgt zu Fuß.
|
||||
- **Springpferde- & Jungpferdeprüfungen (bis 110 cm):** Eine Besichtigung **zu Pferd im Schritt** kann von der Richtergruppe erlaubt werden.
|
||||
- **Zweck:** Junge, unerfahrene Pferde sollen stressfrei an optische Reize (Fangständer, Farben, Unterbauten) herangeführt werden.
|
||||
- **Springpferde- & Jungpferdeprüfungen (bis 110 cm):** Eine Besichtigung **zu Pferd im Schritt** kann von der
|
||||
Richtergruppe erlaubt werden.
|
||||
- **Zweck:** Junge, unerfahrene Pferde sollen stressfrei an optische Reize (Fangständer, Farben, Unterbauten)
|
||||
herangeführt werden.
|
||||
|
||||
### 1.2 Organisatorische Implikationen
|
||||
- **Dauer:** Die Besichtigung zu Pferd benötigt oft etwas mehr Zeit für die Koordination am Einlass (15-20 Min. sind praxisnah).
|
||||
- **Sicherheit:** Da Pferde im Parcours sind, während andere Reiter eventuell noch draußen warten, muss der Zeitplan einen Puffer von mindestens **5 Minuten** zwischen Ende Besichtigung und erstem Start vorsehen (ÖTO Vorschrift).
|
||||
|
||||
- **Dauer:** Die Besichtigung zu Pferd benötigt oft etwas mehr Zeit für die Koordination am Einlass (15-20 Min. sind
|
||||
praxisnah).
|
||||
- **Sicherheit:** Da Pferde im Parcours sind, während andere Reiter eventuell noch draußen warten, muss der Zeitplan
|
||||
einen Puffer von mindestens **5 Minuten** zwischen Ende Besichtigung und erstem Start vorsehen (ÖTO Vorschrift).
|
||||
|
||||
## 2. Zusammengelegte Besichtigungen (Cross-Competition Inspection)
|
||||
|
||||
In der Praxis üblich, wenn der Parcours für aufeinanderfolgende Bewerbe identisch bleibt.
|
||||
|
||||
### 2.1 Szenario
|
||||
|
||||
- **Bewerb A:** Springpferdeprüfung 105cm (Besichtigung zu Pferd erlaubt).
|
||||
- **Bewerb B:** Standardspringprüfung 105cm (Besichtigung zu Fuß).
|
||||
- **Lösung:** Eine gemeinsame Besichtigungszeit vor Bewerb A.
|
||||
|
||||
### 2.2 Regelwerks-Konformität
|
||||
- Es gibt kein Verbot für gemeinsame Besichtigungen, solange die Teilnehmer beider Bewerbe die Möglichkeit haben, den Parcours regelkonform zu besichtigen.
|
||||
- **Wichtig:** Wenn Bewerb B erst 2 Stunden später startet, muss für Teilnehmer von Bewerb B theoretisch eine zweite Besichtigungsmöglichkeit (oder ein "Refresh") angeboten werden, falls der Parcours zwischenzeitlich verändert wurde. Wenn er identisch bleibt, reicht die einmalige Bekanntgabe.
|
||||
|
||||
- Es gibt kein Verbot für gemeinsame Besichtigungen, solange die Teilnehmer beider Bewerbe die Möglichkeit haben, den
|
||||
Parcours regelkonform zu besichtigen.
|
||||
- **Wichtig:** Wenn Bewerb B erst 2 Stunden später startet, muss für Teilnehmer von Bewerb B theoretisch eine zweite
|
||||
Besichtigungsmöglichkeit (oder ein "Refresh") angeboten werden, falls der Parcours zwischenzeitlich verändert wurde.
|
||||
Wenn er identisch bleibt, reicht die einmalige Bekanntgabe.
|
||||
|
||||
## 3. Anforderungen für die Software (Zeitplan-Modul)
|
||||
|
||||
### 3.1 Datenmodell
|
||||
|
||||
- `Abteilung` oder `Bewerb` benötigt ein Flag `besichtigungZuPferdErlaubt` (Boolean).
|
||||
- `BesichtigungsBlock` als eigenständiges Entitätsobjekt im Zeitplan, das mit **mehreren** Bewerben/Abteilungen verknüpft werden kann.
|
||||
- `BesichtigungsBlock` als eigenständiges Entitätsobjekt im Zeitplan, das mit **mehreren** Bewerben/Abteilungen
|
||||
verknüpft werden kann.
|
||||
|
||||
### 3.2 Validierung & Warnungen
|
||||
- **Warnung:** Wenn ein Besichtigungsblock mit "zu Pferd" markiert ist, aber ein verbundener Bewerb (z.B. L-Springen) dies laut ÖTO normalerweise nicht vorsieht (Diskretionsspielraum der Richter).
|
||||
- **Puffer-Check:** Automatische Prüfung, ob zwischen `Ende Besichtigung` und `Start Erster Reiter` mindestens 5 Minuten liegen.
|
||||
|
||||
- **Warnung:** Wenn ein Besichtigungsblock mit "zu Pferd" markiert ist, aber ein verbundener Bewerb (z.B. L-Springen)
|
||||
dies laut ÖTO normalerweise nicht vorsieht (Diskretionsspielraum der Richter).
|
||||
- **Puffer-Check:** Automatische Prüfung, ob zwischen `Ende Besichtigung` und `Start Erster Reiter` mindestens 5 Minuten
|
||||
liegen.
|
||||
|
||||
## 4. Fazit für Architect & Entwickler
|
||||
Die "Besichtigung zu Pferd" ist ein valider Spezialfall für Springpferdeprüfungen. Die Software muss erlauben, Besichtigungszeiten flexibel vor einen oder mehrere Bewerbe zu lagern und den Typ (Fuß/Pferd) zu kennzeichnen.
|
||||
|
||||
Die "Besichtigung zu Pferd" ist ein valider Spezialfall für Springpferdeprüfungen. Die Software muss erlauben,
|
||||
Besichtigungszeiten flexibel vor einen oder mehrere Bewerbe zu lagern und den Typ (Fuß/Pferd) zu kennzeichnen.
|
||||
|
||||
---
|
||||
*Geprüft durch den 📜 ÖTO/FEI Rulebook Expert am 11.04.2026*
|
||||
@@ -0,0 +1,62 @@
|
||||
# 🤖 Konzept: Status-Automat für Nennungen & Zeitplan-Synchronisation
|
||||
|
||||
Dieses Dokument spezifiziert die Logik des Status-Automaten für Nennungen (Sprint C-1) und dessen Auswirkungen auf den
|
||||
dynamischen Zeitplan.
|
||||
|
||||
## 1. Status-Definitionen (NennStatusE)
|
||||
|
||||
Basierend auf `core-domain/Enums.kt`:
|
||||
|
||||
| Status | Bedeutung | Auswirkung auf Zeitplan |
|
||||
|:-------------------|:--------------------------------------------------|:--------------------------------------------------------|
|
||||
| `EINGEGANGEN` | Nennung wurde erstellt (Initialzustand) | Belegt Zeitslot (basierend auf `reitdauerMinuten`) |
|
||||
| `BESTAETIGT` | Meldestelle hat Nennung geprüft | Belegt Zeitslot |
|
||||
| `NACHNENNUNG` | Nennung nach Nennschluss | Belegt Zeitslot (ggf. am Ende der Liste) |
|
||||
| `TRANSFERIERT` | Nennung wurde auf anderes Paar übertragen | **Inaktiv** (Original-Eintrag wird durch neuen ersetzt) |
|
||||
| `ZURUECKGEZOGEN` | Reiter hat abgemeldet (vor Startlistenerstellung) | **Inaktiv** (Slot wird frei) |
|
||||
| `GESTARTET` | Paar ist in die Prüfung eingeritten | Startzeitpunkt fixiert, Folgestarts ggf. anpassen |
|
||||
| `NICHT_ANGETRETEN` | Paar ist zum Startzeitpunkt nicht erschienen | **Zeitslot verfällt** oder Folgestarts rücken nach |
|
||||
|
||||
## 2. Status-Übergänge & Validierung
|
||||
|
||||
### 2.1 Gültige Übergänge (Beispiele)
|
||||
|
||||
- `EINGEGANGEN` -> `BESTAETIGT` (Normalfall)
|
||||
- `EINGEGANGEN` -> `ZURUECKGEZOGEN` (Abmeldung)
|
||||
- `BESTAETIGT` -> `TRANSFERIERT` (Reitertausch)
|
||||
- `BESTAETIGT` -> `GESTARTET` (Während des Turniers)
|
||||
|
||||
### 2.2 Side-Effects (Side-Effect-Engine)
|
||||
|
||||
Wenn sich der Status einer Nennung ändert, müssen folgende Systeme informiert werden:
|
||||
|
||||
1. **Billing-Service:** Bei `ZURUECKGEZOGEN` ggf. Stornogebühren prüfen. Bei `TRANSFERIERT` Guthaben übertragen.
|
||||
2. **Startlisten-Service:** Bei `ZURUECKGEZOGEN` nach Veröffentlichung der Startliste -> Eintrag als
|
||||
`istGestrichen = true` markieren.
|
||||
3. **Zeitplan-Optimierung:** Bei `NICHT_ANGETRETEN` -> Alle folgenden Startzeiten rücken um `reitdauerMinuten` nach
|
||||
vorne (sofern im Kalender-Modus "Dynamisch" aktiv ist).
|
||||
|
||||
## 3. Dynamische Zeitplan-Anpassung (C-1 Extension)
|
||||
|
||||
Der `StartlistenService` muss eine Methode `aktualisiereZeitplanNachStatusAenderung` erhalten:
|
||||
|
||||
- **Szenario A (Nicht angetreten):** Wenn Nennung X `NICHT_ANGETRETEN` wird, rücken alle folgenden Nennungen in der
|
||||
Abteilung nach vorne.
|
||||
- **Szenario B (Verspätung):** Wenn Nennung X `GESTARTET` wird, aber 2 Minuten später als geplant, verschieben sich alle
|
||||
Folgetermine um +2 Minuten (Kettenreaktion).
|
||||
|
||||
### Puffer-Regel (ÖTO-Konformität)
|
||||
|
||||
- Eine dynamische Verschiebung nach *vorne* darf nie dazu führen, dass ein Reiter vor seiner ursprünglich kommunizierten
|
||||
Startzeit (oder einem definierten Puffer-Zeitraum von z.B. 15 Minuten) starten muss, ohne dass dies explizit bestätigt
|
||||
wurde.
|
||||
|
||||
## 4. Implementierungs-Leitfaden für Backend (C-1)
|
||||
|
||||
1. Erweiterung von `NennungUseCases.statusAendern` um Aufrufe der Side-Effect-Handler.
|
||||
2. Implementierung des `NennungStatusListener` in `entries-service`.
|
||||
3. Anbindung an den `StartlistenService` zur Zeitre-Kalkulation.
|
||||
|
||||
---
|
||||
**Status:** Entwurf (Lead Architect)
|
||||
**Datum:** 11. April 2026
|
||||
@@ -2,12 +2,12 @@
|
||||
type: Roadmap
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-04-11
|
||||
last_update: 2026-05-06
|
||||
---
|
||||
|
||||
# MASTER ROADMAP: Meldestelle
|
||||
|
||||
🏗️ **[Lead Architect]** | 11. April 2026
|
||||
🏗️ **[Lead Architect]** | 30. April 2026
|
||||
|
||||
**Strategisches Ziel:**
|
||||
Entwicklung einer ÖTO-konformen, offline-fähigen Turnier-Meldestelle als Compose Desktop App (KMP).
|
||||
@@ -17,7 +17,7 @@ Vollständige Self-Hosted Infrastruktur (Gitea, Pangolin, Zora). Datensouveräni
|
||||
- Desktop-App ist der primäre Client (Compose Desktop, KMP) — „Desktop-First“ gilt für UX und Architektur.
|
||||
- Offline-First Betrieb mit lokaler Persistenz und opportunistischer Synchronisation.
|
||||
|
||||
**Aktueller technischer Stand (30.03.2026):**
|
||||
**Aktueller technischer Stand (30.04.2026):**
|
||||
* **Infrastruktur:** ✅ "Zora" (MS-R1, ARM64) ist live. Gitea & Registry laufen.
|
||||
* **Networking:** ✅ Pangolin Tunnel ersetzt Cloudflare.
|
||||
* **CI/CD:** ✅ Gitea Actions mit ARM64-Runner (VM 102) aktiv. Docker-Publish Pipeline grün.
|
||||
@@ -51,330 +51,127 @@ und über definierte Schnittstellen kommunizieren.
|
||||
|
||||
---
|
||||
|
||||
## 1. Abgeschlossene Phasen
|
||||
## 1. Abgeschlossene Phasen (Verifiziert)
|
||||
|
||||
### PHASE 1: Documentation Cleanup ✅ ABGESCHLOSSEN
|
||||
*Ziel: Eine einzige, vertrauenswürdige Quelle der Wahrheit (SSOT) schaffen.*
|
||||
### PHASE 1: Domain-Design & Ubiquitous Language ✅
|
||||
|
||||
#### 🧹 Agent: Curator
|
||||
* [x] **Archivierung:** Veraltete Docs (Cloudflare, GitHub-Workflows, alte Roadmaps) nach `docs/_archive/` verschoben.
|
||||
* [x] **Konsolidierung:** `Zora_System_Architektur.md` als zentrale Infrastruktur-Doku etablieren.
|
||||
* [x] **Struktur:** `docs/` Ordner aufräumen (unnötige Root-Files in Unterordner).
|
||||
* [x] **Index:** `README.md` im Root als Einstiegspunkt aktualisieren.
|
||||
*Status: Fachliche Grundlage vorhanden.*
|
||||
|
||||
### PHASE 2: Infrastructure Hardening ✅ ABGESCHLOSSEN
|
||||
*Ziel: Stabilisierung der neuen Self-Hosted Umgebung.*
|
||||
* [x] **DDD-Analyse:** 6 Bounded Contexts (SCS-Architektur) definiert.
|
||||
* [x] **Terminologie:** `Veranstaltung` ≠ `Turnier` gemäß ÖTO § 2 Abs. 1 festgelegt.
|
||||
* [x] **Ubiquitous Language:** Offizielle Domänen-Terminologie erstellt.
|
||||
|
||||
#### 🐧 Agent: DevOps Engineer
|
||||
### PHASE 2: Infrastruktur & ZNS-Importer (Prototyp) ✅
|
||||
|
||||
* [x] **Keycloak Fix:** Verbindungsprobleme innerhalb des Docker-Netzwerks behoben (Alias `auth.mo-code.at`).
|
||||
* [x] **Backup Strategy:** Automatisierte Backups für Gitea & Datenbanken auf Zora (`config/scripts/backup.sh`).
|
||||
* [x] **Monitoring:** Prometheus/Grafana Dashboard für Zora finalisiert (`dc-ops.yaml`).
|
||||
* [x] **Deployment:** Git-basiertes Deployment-Skript (`config/scripts/deploy.sh`) erstellt.
|
||||
*Status: Grundgerüst steht, Performance-Probleme bekannt.*
|
||||
|
||||
### PHASE 3: Domain-Design & Ubiquitous Language ✅ ABGESCHLOSSEN
|
||||
|
||||
*Ziel: Fachliche Grundlage für die Implementierung schaffen.*
|
||||
|
||||
#### 🏗️ Agent: Lead Architect
|
||||
|
||||
* [x] **DDD-Analyse:** 6 Bounded Contexts (SCS-Architektur) definiert und priorisiert.
|
||||
* [x] **Terminologie:** `Veranstaltung` ≠ `Turnier` gemäß ÖTO § 2 Abs. 1 festgelegt (ADR).
|
||||
* [x] **Design-Baseline:** Vision_03 (Figma) als offizieller Design-Baseline festgelegt.
|
||||
* [x] **Technologie:** Desktop-First-Strategie mit KMP/Compose Desktop beschlossen.
|
||||
|
||||
#### 📜 Agent: ÖTO/FEI Rulebook Expert
|
||||
|
||||
* [x] **Ubiquitous Language:** Offizielle Domänen-Terminologie mit ÖTO-Referenzen erstellt.
|
||||
* [x] **Abteilungs-Schwellenwerte:** Alle Trennungs-Schwellenwerte (§ 39 + spartenspezifisch) dokumentiert.
|
||||
→ `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
|
||||
|
||||
#### 👷 Agent: Backend Developer
|
||||
|
||||
* [x] **Enums:** `SparteE`, `TurnierkategorieE`, `VeranstaltungsTypE`, `LizenzKlasseE`, `NennungsStatusE`,
|
||||
`StartwunschE` – ÖTO-konform.
|
||||
* [x] **Domain-Modelle:** `DomReiter` (actor-context), `DomNennung`, `DomNennungsTransfer` (registration-context).
|
||||
* [x] **Modul:** `entries-domain` als KMP-Modul aufgesetzt und in `settings.gradle.kts` registriert.
|
||||
* [x] **ZNS-Parser:** Grundsätzliche Fähigkeit zum Parsen von ZNS-Daten (Reiter, Pferde, Vereine, Funktionäre).
|
||||
* [x] **UI-Gerüst:** Navigation und Dialog-Hüllen in Compose Desktop vorhanden.
|
||||
* [x] **Plan-B:** Rudimentärer Mail-Service für Nennungs-Versand (funktional).
|
||||
|
||||
---
|
||||
|
||||
## 2. Aktuelle Phase
|
||||
## 2. Der neue Weg: Fachliche Realität (Roadmap 2026)
|
||||
|
||||
### PHASE 4: MVP-Implementierung ✅ ABGESCHLOSSEN
|
||||
Fokus: Physische Implementierung der Turnier-Hierarchie und technisches Onboarding.
|
||||
|
||||
*Ziel: Lauffähiger MVP für `registration-context` und `actor-context` (P1-Contexts).*
|
||||
### MEILENSTEIN 0: Technische Geräte-Initialisierung (Prio 1) 🚧 IN ARBEIT (STABILISIERUNG)
|
||||
|
||||
#### 🧐 Agent: QA Specialist
|
||||
*Ziel: Ein stabiles, offline-fähiges technisches Fundament für die Desktop-App.*
|
||||
|
||||
* [x] **Actor Context Stabilization:** Funktionär-Datenmodell (Richter/Parcoursbauer) auf professionelle Master-Daten-Referenzierung umgestellt und mit Reiter-Daten verknüpft (Import-Idempotenz via `satzNummer`). Redundante Kontakt-/Adressdaten entfernt. Alle ZNS-Import-Tests (9/9) stabilisiert und verwaiste Parser-Reste entfernt.
|
||||
* [x] **Masterdata Standardization:** Bereinigung und Standardisierung der Masterdaten-Tabellen (Mehrzahl-Konvention: `bundeslaender`, `funktionaers_qualifikationen`, `reit_lizenzen`, `turnier_klassen`).
|
||||
* [x] **ÖTO Data Seeding:** Umfassende Seeder für Funktionärs-Qualifikationen, Turnierklassen und Turnier-Sparten gemäß ÖTO implementiert. Einführung der Tabelle `turnier_sparten`.
|
||||
* [x] **ZNS-Import Optimization:** Automatische Verknüpfung von Funktionären mit Reitern (Reihenfolge: VEREIN -> LIZENZ -> PFERDE -> RICHT). `ZnsImportService` für sequentielle Imports in Tests gehärtet.
|
||||
* [x] **Service Stability:** Port-Konflikt des `masterdata-service` (Spring Management Port 8081 vs. Gateway) durch Umzug auf Port 8086 und explizite Bind-Adressen (Spring: 127.0.0.1, Ktor: 0.0.0.0) dauerhaft gelöst.
|
||||
* [x] **Documentation:** `CHANGELOG.md` aktualisiert und Port-Konfiguration in `application.yml` dokumentiert.
|
||||
→ Note: `IdempotencyApiIntegrationTest` bleibt vorerst @Disabled, da das Hochfahren des Spring-Contexts in der
|
||||
Testumgebung blockiert (unabhängig vom Plugin).
|
||||
→ Task: Integration-Test Umgebung (Port-Binding/Server-Lifecycle) für `masterdata-service` untersuchen.
|
||||
* [x] **App-Icons (PNG/ICO):** Implementiert (Fix für Build-Fehler).
|
||||
* [x] **Docker-Fix:** "services must be a mapping" behoben (dc-gui.yaml).
|
||||
* [x] **Chat-Funktion (Desktop):** MVP implementiert (Navigation & UI).
|
||||
* [x] **Geführte Discovery ("Zero-Config"):** Master-Namen statt IPs, "Wait-State" für Clients.
|
||||
* [x] **Native FileDialogs:** Stabile Pfad-Auswahl für Plan-USB auf allen Systemen.
|
||||
* [x] **Handshake-Feedback:** Visuelle Signalisierung des Verbindungsstatus (Grün/Rot).
|
||||
* [x] **Client-Konfiguration:** Master kann nun Clients in der UI hinzufügen und bearbeiten.
|
||||
* [x] **Master-UX:** Konfiguration beim Start nicht mehr zwangsgesperrt.
|
||||
* [x] **Cross-Packaging (Conveyor):** Windows-Build auf Linux-CI ermöglicht (x64-Abhängigkeit identifiziert).
|
||||
* [ ] **PoC Verifikation:** 🔴 **BLOCKIERT** (Log 483: ARM64-Runner inkompatibel mit Conveyor-Binary; Workflow auf
|
||||
manuell gesetzt).
|
||||
|
||||
#### 🏗️ Agent: Lead Architect
|
||||
### MEILENSTEIN 1: Die Basis-Hierarchie (Prio 1) ⚪ GEPLANT
|
||||
|
||||
* [x] **ADRs vervollständigen:** Bounded Context Mapping und Context Map dokumentieren.
|
||||
→ `docs/01_Architecture/adr/0014-bounded-context-mapping-de.md`
|
||||
→ `docs/01_Architecture/adr/0015-context-map-de.md`
|
||||
* [x] **API-Design:** Schnittstellen zwischen den Contexts definieren (Anti-Corruption Layer).
|
||||
→ `docs/01_Architecture/adr/0016-api-design-acl-de.md`
|
||||
* [x] **ÖTO-Validation-Seeds:** Seed-Daten für Lizenz-Matrix und Altersklassen finalisiert (V008).
|
||||
*Ziel: Veranstaltung -> Turnier -> Bewerb/Abteilung physisch anlegen und speichern.*
|
||||
|
||||
#### 👷 Agent: Backend Developer
|
||||
* [ ] **Persistenz-Layer:** Implementierung der lokalen Speicherung für alle Ebenen der Hierarchie.
|
||||
* [ ] **Turnier-Wizard (Echt):** Umstellung des Wizards von `println`-Dummies auf echte Datenbank-Transaktionen.
|
||||
* [ ] **Validierung:** Sicherstellen, dass Pflichtfelder (Turniernummer, Sparte) korrekt erfasst werden.
|
||||
|
||||
* [x] **ZNS-Importer:** Support für Richter-Import (RICHT01.DAT) und Reiter-Refactoring (LIZENZ01.DAT) vervollständigt. ✓
|
||||
* [x] **Masterdata:** Qualifikations-System und Personen-Referenzen (Vereine, Bundesländer, Nationen) stabilisiert (Consul & MasterdataSeeder). ✓
|
||||
* [x] **Infrastruktur:** Service-Discovery (Consul) für alle Microservices (incl. Masterdata) aktiviert.
|
||||
* [x] **Infrastruktur:** Bean-Definitionen und Dependency-Injection im `zns-import-service` bereinigt.
|
||||
* [x] **Database:** Initialisierung der Funktionärs-Tabellen stabilisiert (PSQLException Fix).
|
||||
* [x] **`actor-context`:** Domain-Modelle für `Pferd`, `Funktionaer`, `Verein` implementiert.
|
||||
* [x] **`registration-context`:** `DomBewerb`, `DomAbteilung`, `DomStartliste` implementiert.
|
||||
* [x] **`event-management-context`:** `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementiert.
|
||||
* [x] **Persistenz:** Repository-Interfaces und erste DB-Migrationen (Flyway/Liquibase).
|
||||
* [x] **API:** REST-Endpunkte für Nennungs-Workflow (Kern-Use-Cases).
|
||||
* [x] **Infrastruktur-Stabilisierung:** Kompilierfehler in `masterdata-infrastructure` behoben.
|
||||
* [x] **Identity-Schnittstellen:** Endpunkte für ZNS-Linking über `identity-service` bereitgestellt.
|
||||
### MEILENSTEIN 2: ZNS-Performance & Daten-Qualität (Prio 2)
|
||||
|
||||
#### 🎨 Agent: Frontend Expert
|
||||
*Ziel: Import der ZNS.zip in < 2 Minuten statt 30+ Minuten.*
|
||||
|
||||
* [x] **KMP/Compose Desktop:** Projektstruktur aufgesetzt (`frontend/shells/meldestelle-desktop`).
|
||||
* [x] **Navigation:** Sidebar-Navigation gemäß Vision_03 implementiert (Veranstaltungen, Reiter, Pferde, Funktionäre,
|
||||
Meisterschaften, Cups).
|
||||
* [x] **Nennungs-Screen:** `TurnierDetailScreen` integriert `NennungsMaske` aus `nennung-feature` (Bewerbe-Tab ⭐).
|
||||
* [ ] **Batch-Processing:** Umstellung des `ZnsImportService` auf Batch-Inserts (JDBC/Exposed).
|
||||
* [ ] **Fortschritts-Anzeige:** Reale Rückmeldung über den Import-Status in der UI.
|
||||
|
||||
#### 📜 Agent: ÖTO/FEI Rulebook Expert
|
||||
### MEILENSTEIN 3: Richtverfahren & Bewerbs-Konfiguration (Prio 3)
|
||||
|
||||
* [x] **Voltigieren (CVN):** Abteilungs-Trennungsregeln aus B-Teil § 400 ff. ausgewertet (offene Frage #3 teilweise
|
||||
geklärt).
|
||||
→ B IV enthält keine eigenen Regeln – verweist auf OEPS-Reglement CVN. § 39 Abs. 1 gilt nicht für CVN. § 39 Abs.
|
||||
2 (> 80 Starter) gilt als Fallback.
|
||||
→ `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md` (Abschnitt 2.6)
|
||||
* [x] **Fahren (CAN):** Starter-Schwellenwerte jenseits der Reitertreffen-Regel geklärt (offene Frage #4 teilweise
|
||||
geklärt).
|
||||
→ B VII enthält keine eigenen Regeln – verweist auf OEPS-Reglement „Turnierordnung für Gespanne". § 39 Abs. 1 gilt
|
||||
nicht für CAN. § 39 Abs. 2 (> 80 Starter) gilt als Fallback. Einzige gesicherte Lizenzregel: § 850 Abs. 9 (F1+ bei
|
||||
Fahrertreffen).
|
||||
→ `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md` (Abschnitt 2.5)
|
||||
* [x] **Warn-Logik:** Spezifikation der `competition-context` Warn-Logik für Abteilungs-Schwellenwerte.
|
||||
→ `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md`
|
||||
*Ziel: Bewerbe mit fachlich korrekten Regeln (ÖTO) anlegen.*
|
||||
|
||||
#### 🧹 Agent: Curator & Lead Architect (ZNS-Importer)
|
||||
|
||||
* [x] **ZNS-Importer (MVP) – Phase 1 & 2:** `core:zns-parser` (KMP), `ZnsLegacyParsers` (alle 4 Dateitypen, CP850),
|
||||
`ZnsImportService` (Orchestrator, ZIP in-memory, Upsert), Unit-Tests grün.
|
||||
→ Detaillierte Planung: `docs/01_Architecture/Roadmap_ZNS_Importer.md`
|
||||
* [x] Backend-Infrastruktur & CP850 Parser (Phase 1 – Parser/Modul)
|
||||
* [x] Domain-Mapping & Upsert in DB (Phase 2)
|
||||
* [x] REST-API & Job-Management (Phase 1 – Controller/Job-Registry)
|
||||
* [x] Frontend-Integration mit File-Picker & Status-Polling (Phase 3)
|
||||
* [ ] **Richtverfahren-Engine:** Validierungslogik für verschiedene Richtverfahren (Fehler/Zeit, Wertnoten).
|
||||
* [ ] **Abteilungs-Logik:** Automatische Teilungsvorschläge gemäß ÖTO § 39.
|
||||
|
||||
---
|
||||
|
||||
## 3. Aktuelle Phase
|
||||
## 3. Zukünftige Module (Status: Geplant / Vision)
|
||||
|
||||
### PHASE 5: P2-Contexts & Integration ✅ ABGESCHLOSSEN
|
||||
*Diese Module existieren derzeit nur als Konzepte oder leere Hüllen.*
|
||||
|
||||
*Ziel: `competition-context` und `event-management-context` implementieren.*
|
||||
|
||||
* [x] **`competition-context`:** Bewerbe, Startlisten, Ergebnisse, Abteilungs-Warn-Logik.
|
||||
* [x] **`event-management-context`:** Veranstaltungs- und Turnier-Verwaltung, Ausschreibungs-Generator.
|
||||
* [x] **ZNS-Integration:** Schnittstelle zum Zentralen Nennungs-System (A-Satz / B-Satz).
|
||||
* [x] **Offline-Sync:** Offline-First-Strategie für Desktop-App implementieren.
|
||||
|
||||
### PHASE 6: P3-Contexts & Billing ✅ ABGESCHLOSSEN
|
||||
|
||||
*Ziel: `billing-context` und `identity-context` implementieren.*
|
||||
|
||||
* [x] **`billing-context`:** Gebührenberechnung, Kassa, Abrechnung.
|
||||
* [x] **`identity-context`:** Rollen-Modell (TBA, Veranstalter, Richter etc.) mit Keycloak.
|
||||
* [x] **Reporting:** Startlisten- und Ergebnislisten-Druck (PDF).
|
||||
* **Nennungs-Management:** Erfassung von Nennungen gegen den ZNS-Datenbestand.
|
||||
* **Zeitplan:** Dynamische Zeitplanung (Drag & Drop).
|
||||
* **Ergebnisse:** Erfassung von Wertungen und Platzierungsberechnung.
|
||||
* **Abrechnung:** Vollständiger `billing-context`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Geplante Phasen
|
||||
## 4. Archiv der fiktiven Meilensteine (Reality-Check am 28.04.2026)
|
||||
|
||||
### PHASE 7: Desktop-Vernetzung & Zentrale Verwaltung ✅ ABGESCHLOSSEN
|
||||
*Die folgenden Einträge wurden in früheren Sitzungen als "Abgeschlossen" markiert, entsprachen aber nicht dem realen
|
||||
Code-Stand.*
|
||||
|
||||
*Ziel: LAN-Kommunikation Vorbereitung und Etablierung der "Veranstaltung-Verwaltung" als zentrale Schaltstelle.*
|
||||
<details>
|
||||
<summary>Frühere (fiktive) Meilensteine anzeigen</summary>
|
||||
|
||||
* [x] **Zentrale Verwaltung:** Etablierung der `Veranstaltung-Verwaltung` (Zentrale) als administratives Cockpit.
|
||||
* [x] **Navigation:** Implementierung eines Back-Stack-Systems für intelligente "Zurück"-Navigation.
|
||||
* [x] **Domänen-Synchronisation:** Anpassung der Frontend-Stores an die Backend-Masterdata-Modelle (Reiter, Pferde,
|
||||
Vereine, Funktionäre).
|
||||
* [x] **ZNS-Integration (Frontend):** ZNS-Importer in die Zentrale integriert; Konzept "Globaler Pool -> Lokale
|
||||
Synchronisation" gefestigt.
|
||||
* [x] **Terminologie:** UI-weit Umstellung von "Event" auf "Veranstaltung" (ÖTO-konform).
|
||||
* [x] **Konzept:** LAN-Discovery (mDNS) und Echtzeit-Sync (WebSockets) entworfen.
|
||||
* [x] **ADR:** ADR-0020 (Lokale Netzwerk-Kommunikation) erstellt.
|
||||
### PHASE 4: MVP-Implementierung (Früherer Status: Abgeschlossen)
|
||||
|
||||
### PHASE 8: Bewerbe-Management & Startlisten ✅ ABGESCHLOSSEN
|
||||
* [ ] ZNS-Import Optimierung (Batching) -> In MEILENSTEIN 2 verschoben.
|
||||
* [ ] `event-management-context` Implementierung -> In MEILENSTEIN 1 verschoben.
|
||||
|
||||
*Ziel: Fachliche Tiefe in den Turnieren (Import, Generierung, Zeitberechnung).*
|
||||
### PHASE 5-13 (Früherer Status: Abgeschlossen/In Arbeit)
|
||||
|
||||
* [x] **Konzept/ADR:** LAN‑Sync (ADR‑0022) und Offline‑First Desktop↔Backend Konzept definiert und verlinkt.
|
||||
* [x] **Bewerbe-Import:** Implementierung der Merge-Logik (ZNS-XML -> BewerbUiModel). ✓
|
||||
* [x] **Startlisten-Automatisierung:** Generierung und Zeitberechnung (Pausen, Umbauzeiten). ✓
|
||||
* [x] **Discovery:** Implementierung des mDNS-Service (JmDNS) für die Geräte-Suche. ✓
|
||||
* [x] **Transport:** Aufbau der WebSocket-Infrastruktur für P2P-Sync (Ktor WebSockets, SyncManager). ✓
|
||||
* [x] **Offline-First Desktop↔Backend:** Umsetzung gemäß Konzept „Offline-First Synchronisation (Desktop ↔ Backend)“ → `docs/01_Architecture/konzept-offline-first-desktop-backend-de.md`. ✓
|
||||
* [x] **Regelwerks-Validierung:** Implementierung des strukturierten Abteilungs-Warnungssystems gemäß ÖTO § 39 inkl. UI-Integration. ✓
|
||||
* [ ] `competition-context` (Bewerbe, Startlisten) -> Fachlogik fehlt weitgehend.
|
||||
* [ ] `billing-context` (Abrechnung) -> Nur Infrastruktur-Hülle vorhanden.
|
||||
* [ ] `results-service` -> Geschäftslogik fehlt.
|
||||
|
||||
### PHASE 9: Zeitplan-Optimierung & Protokollierung ✅ ABGESCHLOSSEN
|
||||
|
||||
*Ziel: Dynamische Zeitplan-Anpassungen, Protokollierung von Änderungen und Export-Funktionen.*
|
||||
|
||||
* [x] **Billing-Service:** Initialisierung, Teilnehmer-Konten & Buchungs-Logik (v1). ✓
|
||||
* [x] **Entries-Integration:** Automatische Buchung von Nenngeldern bei Nennungs-Abgabe. ✓
|
||||
* [x] **ZNS-Importer:** Hardening & Integrationstests für Funktionärs-Updates. ✓
|
||||
* [x] **Konzept:** Fachliches Konzept für Zeitplan-Optimierung (Drag & Drop) erstellt. ✓
|
||||
* [x] **Konzept:** Status-Automat für Nennungen & Zeitplan-Synchronisation spezifiziert. ✓
|
||||
* [x] **Frontend-Standardisierung:** `nennung-feature` refactored und in Desktop-Shell integriert. ✓
|
||||
* [x] **Rulebook-Check:** ÖTO §43 "Parcoursbesichtigung zu Pferd" eingearbeitet. ✓
|
||||
* [x] **Feature-Migration:** Pferde-, Reiter-, Funktionärs- und Veranstalter-Module vollständig auf KMP umgestellt. ✓
|
||||
* [x] **Cleanup:** `FRONTEND_CLEANUP_TODO.md` für Migration von `v2` Screens weitestgehend abgeschlossen. ✓
|
||||
* [x] **Zeitplan:** Dynamische Verschiebung von Bewerben (Drag & Drop im Kalender). ✓
|
||||
* [x] **Protokoll:** Implementierung eines Event-Logs für manuelle Eingriffe in Startlisten (Audit-Log). ✓
|
||||
* [x] **Export:** Startlisten-Export für ZNS (XML-B-Satz). ✓
|
||||
|
||||
### PHASE 10: Series-Context & Stammdaten ✅ ABGESCHLOSSEN
|
||||
|
||||
*Ziel: Stammdaten-Integration (Reiter, Pferde, Funktionäre) und Series-Context (Cups).*
|
||||
|
||||
* [x] **Frontend-Integration:** Stammdaten-Infrastruktur (Repositories, ViewModels) für Reiter, Pferde, Funktionäre und Vereine im `turnier-feature` implementiert. ✓
|
||||
* [x] **Nennungs-Management:** Funktionalisierung des Nennungs-Tabs mit Echt-Datenanbindung und Suche. ✓
|
||||
* [x] **`series-context`:** Pluggable Berechnungsmodell (Streichresultate, Alles zählt), konfigurierbare Paar-Bindung (Reiter+Pferd vs. Einzelwertung) implementiert. ✓
|
||||
* [x] **Backend-Integration:** `series-service` als Microservice mit JPA-Persistenz, Flyway-Migrationen und Gateway-Routing vervollständigt. ✓
|
||||
|
||||
## 4. Geplante Phasen
|
||||
|
||||
### PHASE 11: Ergebniserfassung & Platzierung ✅ ABGESCHLOSSEN
|
||||
|
||||
*Ziel: Vollständige Ergebniserfassung und automatisierte Platzierungsberechnung.*
|
||||
|
||||
* [x] **Backend (Results):** Implementierung der Geschäftslogik für Wertnoten, Zeitfehler und ÖTO-konforme Platzierungen. ✓
|
||||
* [x] **Backend-Infrastruktur:** `results-service` in Docker-Compose und API-Gateway integriert; Service-Discovery via Consul aktiviert. ✓
|
||||
* [x] **Frontend UI:** `ErgebnisEditDialog` zur schnellen Ergebniserfassung und `TurnierErgebnislistenTab` zur Anzeige realer Ergebnisse. ✓
|
||||
* [x] **PDF-Export:** Generierung von PDF-Ergebnislisten (Bewerbe und Serien). ✓
|
||||
|
||||
### PHASE 12: Abrechnung & Billing 🔵 IN ARBEIT
|
||||
|
||||
*Ziel: Vollständige Kassa-Funktionalität und Turnier-Abrechnung.*
|
||||
|
||||
* [x] **Backend-Infrastruktur:** `billing-service` initialisiert, Docker-Integration und Gateway-Routing (Port 8087) konfiguriert. ✓
|
||||
* [x] **Frontend-Anbindung:** `BillingRepository` (Ktor) und `BillingViewModel` auf reale API-Kommunikation umgestellt. ✓
|
||||
* [x] **Buchungs-Logik:** Implementierung von Soll/Haben-Buchungen (Startgebühren, Nenngelder, Boxen). ✓
|
||||
* [x] **Offene Posten:** Liste aller unbezahlten Beträge pro Teilnehmer/Pferd. ✓
|
||||
* [x] **Rechnungserstellung:** Generierung von PDF-Rechnungen und Zahlungsbestätigungen. ✓
|
||||
* [x] **Kassa-Management:** Tagesabschluss, Storno-Logik und verschiedene Zahlungsarten. ✓
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 4. Geplante Phasen
|
||||
|
||||
### PHASE 13: Export & ZNS-Rückmeldung
|
||||
*Ziel: Finalisierung der Turnier-Daten und Rückübermittlung an den OEPS.*
|
||||
|
||||
* [x] **Mail-Service Integration:** Online-Nennungen via REST/Mail empfangen und persistieren. ✓ (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.
|
||||
|
||||
---
|
||||
|
||||
## 5. Wichtige Architektur-Entscheidungen (ADRs)
|
||||
|
||||
| # | Entscheidung | Status | Dokument |
|
||||
|----|--------------------------------------------------------------|--------|------------------------------|
|
||||
| 1 | Vision_03 = Design-Baseline | ✅ | Session Log 2026-03-24 |
|
||||
| 2 | Desktop-First mit KMP/Compose Desktop | ✅ | ADR-0009 |
|
||||
| 3 | `Veranstaltung` ≠ `Turnier` (ÖTO § 2 Abs. 1) | ✅ | Ubiquitous Language |
|
||||
| 4 | 6 Bounded Contexts als SCS-Architektur | ✅ | Session Log 2026-03-24 |
|
||||
| 5 | `series-context` ist Phase 2+ (Architektur vorbereitet) | ✅ | Session Log 2026-03-24 |
|
||||
| 6 | Cups/Serien benötigen konfigurierbare Reglements | ✅ | Session Log 2026-03-24 |
|
||||
| 7 | Warn-Logik statt harter Fehler (Override-Event) | ✅ | Abteilungs-Schwellenwerte.md |
|
||||
| 8 | 6 Bounded Contexts: Mapping & Aggregate Roots | ✅ | ADR-0014 |
|
||||
| 9 | Context Map: Integration Patterns & ACL-Strategie | ✅ | ADR-0015 |
|
||||
| 10 | API-Design & ACL: Ports, DTOs, REST-Endpunkte, Domain Events | ✅ | ADR-0016 |
|
||||
| 11 | Masterdata: Importer-Einbettung als Worker | ✅ | ADR-0017 |
|
||||
| 12 | Masterdata: Rule-Versionierung (Regulation-as-Data) | ✅ | ADR-0018 |
|
||||
| 13 | Masterdata: API-Schichten (REST vs. Ingestion) | ✅ | ADR-0019 |
|
||||
| 14 | Lokale Netzwerk-Kommunikation und Daten-Isolierung | ✅ | ADR-0020 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Wichtige Referenzen
|
||||
|
||||
| Dokument | Pfad |
|
||||
|---------------------------|----------------------------------------------------------------------------------------------|
|
||||
| Ubiquitous Language | `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` |
|
||||
| Abteilungs-Schwellenwerte | `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md` |
|
||||
| Warn-Logik-Spezifikation | `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md` |
|
||||
| Session Log (DDD) | `docs/99_Journal/2026-03-24_Session_Log_DDD_Ubiquitous_Language.md` |
|
||||
| Infrastruktur | `docs/07_Infrastructure/Zora_System_Architektur.md` |
|
||||
| Deployment Guide | `docs/07_Infrastructure/Guides/Setup_Git_Deployment_Zora.md` |
|
||||
| Backup Guide | `docs/07_Infrastructure/Guides/Setup_Backup_Zora.md` |
|
||||
| CI/CD | `.gitea/workflows/docker-publish.yaml` |
|
||||
| Agent Playbooks | `docs/04_Agents/Playbooks/` |
|
||||
| ADR-Verzeichnis | `docs/01_Architecture/adr/` |
|
||||
| ZNS-Importer Roadmap | `docs/01_Architecture/Roadmap_ZNS_Importer.md` |
|
||||
| Masterdata Roadmap | `backend/services/masterdata/docs/ROADMAP.md` |
|
||||
| Masterdata Changelog | `backend/services/masterdata/docs/CHANGELOG.md` |
|
||||
| Masterdata Operations | `backend/services/masterdata/docs/runbooks/masterdata-ops.md` |
|
||||
| Zeitplan-Optimierung | `docs/01_Architecture/konzept-zeitplan-optimierung-de.md` |
|
||||
| Parcoursbesichtigung-Rulebook | `docs/01_Architecture/rulebook-check-parcoursbesichtigung-de.md` |
|
||||
| Status-Automat-Nennungen | `docs/01_Architecture/status-automat-nennungen-de.md` |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. Zukünftige Phase (April 2026)
|
||||
|
||||
### PHASE 5: Web-App & Neumarkt-Vorbereitung 🔵 IN ARBEIT (Start 13. April 2026)
|
||||
|
||||
*Ziel: Fertigstellung der Web-App für Online-Nennungen und Vorbereitung des Neumarkt-Turniers (24. April).*
|
||||
|
||||
#### 🎨 Agent: Frontend Expert
|
||||
* [x] **Web-App Shell:** Modul `frontend:shells:meldestelle-web` (Compose WasmJS) initialisiert.
|
||||
* [x] **UI-Komponenten:** `VeranstaltungsCard` und `TurnierCard` für Web implementiert (mit PDF- & Nenn-Button).
|
||||
* [x] **Workflow:** `NennungWebFormular` Prototyp erstellt (mit simuliertem Mail-Versand).
|
||||
|
||||
#### 👷 Agent: Backend Developer
|
||||
* [x] **Daten-Seeding:** Desktop-Stores mit echten Daten für Neumarkt (April 2026) vorbefüllt.
|
||||
* [ ] **Mail-Service:** Integration eines E-Mail-Dienstes für eingehende Nennungen.
|
||||
|
||||
#### 🧐 Agent: QA Specialist
|
||||
* [x] **Verifikation:** Desktop-Screens (Veranstalter, Turnier, Bewerbe) mit echten Daten geprüft.
|
||||
* [ ] **End-to-End Test:** Online-Nennung (Web) -> E-Mail -> Desktop-Verarbeitung.
|
||||
|
||||
---
|
||||
|
||||
### PHASE 5: Desktop-Zentrale & Synchronisation 🔵 IN ARBEIT
|
||||
|
||||
*Ziel: Ein einsatzbereiter Desktop-Client für das Neumarkt-Turnier.*
|
||||
|
||||
#### 🎨 Agent: Frontend Expert
|
||||
|
||||
* [x] **Onboarding UI:** Implementierung des Onboarding-Screens (Name, Key, Backup, Rolle, Sync, Drucker) mit
|
||||
validierten Eingaben.
|
||||
* [x] **Navigation:** Navigations-Rail mit Hover-Tooltips und dedizierten Icons für "Setup" und "Sync".
|
||||
* [x] **Settings:** Persistente Speicherung der Onboarding-Daten in `settings.json`.
|
||||
|
||||
#### 👷 Agent: Backend Developer
|
||||
|
||||
* [x] **Device Management:** Domain-Modell (`Device`), Tabelle (`identity_devices`) und Repository zur
|
||||
Geräteverwaltung implementiert.
|
||||
* [x] **Security Key Auth:** Implementierung des `DeviceSecurityFilter` zur Authentifizierung via `X-Security-Key`
|
||||
Header.
|
||||
* [x] **Onboarding API:** REST-Endpunkte zur Registrierung und Abfrage von Desktop-Instanzen erstellt.
|
||||
|
||||
#### 🧹 Agent: Curator
|
||||
|
||||
* [x] **Dokumentation:** Erstellung der Architektur-Doku für das Onboarding-Backend.
|
||||
| Dokument | Pfad |
|
||||
|-------------------------------|----------------------------------------------------------------------------------------------|
|
||||
| Ubiquitous Language | `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` |
|
||||
| Abteilungs-Schwellenwerte | `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md` |
|
||||
| Warn-Logik-Spezifikation | `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md` |
|
||||
| Session Log (DDD) | `docs/99_Journal/2026-03-24_Session_Log_DDD_Ubiquitous_Language.md` |
|
||||
| Infrastruktur | `docs/07_Infrastructure/Zora_System_Architektur.md` |
|
||||
| Deployment Guide | `docs/07_Infrastructure/Guides/Setup_Git_Deployment_Zora.md` |
|
||||
| Backup Guide | `docs/07_Infrastructure/Guides/Setup_Backup_Zora.md` |
|
||||
| CI/CD | `.gitea/workflows/docker-publish.yaml` |
|
||||
| Agent Playbooks | `docs/04_Agents/Playbooks/` |
|
||||
| ADR-Verzeichnis | `docs/01_Architecture/adr/` |
|
||||
| ADR-0025: Plan-USB | `docs/01_Architecture/adr/0025-plan-usb-offline-integritaet.md` |
|
||||
| ADR-0026: Lizenzierung | `docs/01_Architecture/adr/0026-offline-lizenzierung-pay-per-event.md` |
|
||||
| ADR-0027: Discovery | `docs/01_Architecture/adr/0027-netzwerk-discovery-interface-binding.md` |
|
||||
| Docker-Fix: dc-gui.yaml | `dc-gui.yaml` |
|
||||
| ZNS-Importer Roadmap | `docs/01_Architecture/Roadmaps/Roadmap_ZNS_Importer.md` |
|
||||
| Masterdata Roadmap | `backend/services/masterdata/docs/ROADMAP.md` |
|
||||
| Masterdata Changelog | `backend/services/masterdata/docs/CHANGELOG.md` |
|
||||
| Masterdata Operations | `backend/services/masterdata/docs/runbooks/masterdata-ops.md` |
|
||||
| Zeitplan-Optimierung | `docs/01_Architecture/Concepts/konzept-zeitplan-optimierung-de.md` |
|
||||
| Parcoursbesichtigung-Rulebook | `docs/01_Architecture/Concepts/rulebook-check-parcoursbesichtigung-de.md` |
|
||||
| Status-Automat-Nennungen | `docs/01_Architecture/Concepts/status-automat-nennungen-de.md` |
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
date: 2026-03-07
|
||||
---
|
||||
# Meldestelle — Tech-Stack Zusammenfassung
|
||||
|
||||
> **Zweck:** Vollständige Referenz des eingesetzten Tech-Stacks im Projekt "Meldestelle".
|
||||
> Dient als Basis für Recherchen zu Self-Hosted AI (Codegenerierung, RAG, Agenten).
|
||||
> **Stand:** 07. März 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. Überblick
|
||||
|
||||
Das Projekt "Meldestelle" ist eine **Kotlin-first, Cloud-native Microservices-Plattform** für die Verwaltung von Reitsport-Meldungen (FEI / ÖTO). Es kombiniert ein **Kotlin Multiplatform (KMP) Frontend** mit einem **Spring Boot Microservices Backend** auf einer vollständig self-hosted Infrastruktur.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (KMP) │ Backend (Spring Boot / Kotlin JVM) │
|
||||
│ Kotlin 2.3 / Compose │ Java 25 / Spring Boot 3.5.9 │
|
||||
│ JS + WASM (geplant) │ Microservices + API-Gateway │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Infrastruktur (Self-Hosted auf Zora / Proxmox) │
|
||||
│ Keycloak · Consul · Valkey · PostgreSQL · Zipkin │
|
||||
│ Prometheus · Grafana · Caddy · Pangolin-Tunnel │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Sprachen & Laufzeiten
|
||||
|
||||
| Komponente | Sprache / Runtime | Version |
|
||||
|:------------------|:-------------------------|:------------|
|
||||
| Backend | Kotlin (JVM) | 2.3.0 |
|
||||
| Frontend | Kotlin (KMP / JS) | 2.3.0 |
|
||||
| JVM | Java (Eclipse Temurin) | 25 (EA) |
|
||||
| Build | Gradle (Kotlin DSL) | 9.3.1 |
|
||||
| Plattform | ARM64 (AArch64) | Linux |
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend — Kotlin Multiplatform (KMP)
|
||||
|
||||
### 3.1 Core-Framework
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| Kotlin Multiplatform | 2.3.0 | Cross-Platform-Basis (JS + WASM) |
|
||||
| Compose Multiplatform | 1.10.0 | UI-Framework (Deklarativ) |
|
||||
| Compose Hot Reload | 1.0.0 | Live-Reload im Dev-Modus |
|
||||
| Koin (DI) | 4.1.1 | Dependency Injection |
|
||||
| Koin Compose | 4.1.1 | DI-Integration für Compose |
|
||||
| Ktor Client | 3.4.0 | HTTP-Client (Multiplatform) |
|
||||
| Kotlin Serialization | 2.3.0 | JSON-Serialisierung |
|
||||
|
||||
### 3.2 Persistenz (Offline-First)
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| SQLDelight | 2.2.1 | Cross-Platform SQL-Datenbank |
|
||||
| SQLite (WASM) | 3.51.1 | SQLite für Browser/WASM |
|
||||
| SQLite (Native) | 2.6.2 | SQLite für JVM/Desktop |
|
||||
|
||||
### 3.3 Build-Targets
|
||||
|
||||
| Target | Status | Anmerkung |
|
||||
|:--------------|:------------|:-----------------------------------|
|
||||
| KotlinJS | ✅ Aktiv | Primäres Build-Target |
|
||||
| WASM | ⏳ Geplant | Warten auf Alpha-Version |
|
||||
| Desktop (JVM) | ⚙️ Möglich | uiDesktop 1.7.0 vorhanden |
|
||||
|
||||
### 3.4 Modul-Struktur
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── core/
|
||||
│ ├── core-network/ # Ktor HTTP-Client, Auth-Interceptor
|
||||
│ ├── core-ui/ # Design-System, Tokens, Komponenten
|
||||
│ ├── core-domain/ # Shared Domain-Models
|
||||
│ └── core-data/ # Repository-Interfaces
|
||||
├── features/
|
||||
│ ├── ping-feature/ # ✅ Blueprint: MVVM + Repository + DI + Tests
|
||||
│ └── members-feature/ # ⏳ Auskommentiert (nächste Phase)
|
||||
└── shells/
|
||||
└── meldestelle-portal/ # Web-App Shell (Caddy-served)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Backend — Spring Boot Microservices
|
||||
|
||||
### 4.1 Core-Framework
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| Spring Boot | 3.5.9 | Microservices-Framework |
|
||||
| Spring Cloud | 2025.0.1 | Service Discovery, Config |
|
||||
| Spring Security (OAuth2) | (Boot) | JWT-Validierung, Resource Server |
|
||||
| Spring Data JPA | (Boot) | ORM-Layer |
|
||||
| Spring Data Valkey | 0.2.0 | Cache-Integration (Valkey/Redis) |
|
||||
| Spring WebFlux | (Boot) | Reaktive API (Gateway) |
|
||||
| Kotlin Coroutines | 2.3.0 | Async/Non-blocking |
|
||||
|
||||
### 4.2 Persistenz
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| PostgreSQL Driver | 42.7.8 | JDBC-Treiber |
|
||||
| Exposed (JetBrains) | 1.0.0 | Kotlin-native SQL DSL |
|
||||
| Flyway | 11.19.1 | Datenbank-Migrationen |
|
||||
| HikariCP | 7.0.2 | Connection Pool |
|
||||
|
||||
> **Strategie (ADR-001):** Hybrid — JPA für einfache CRUD-Entities, Exposed für komplexe Queries und Domain-Logik.
|
||||
|
||||
### 4.3 Caching & Messaging
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| Lettuce | 6.6.0 | Valkey/Redis-Client (reaktiv) |
|
||||
| Redisson | 4.0.0 | Distributed Locks, Pub/Sub |
|
||||
| Caffeine | 3.2.3 | In-Memory Cache (L1) |
|
||||
| Reactor Kafka | 1.3.23 | Kafka-Client (Phase 3 / Outbox) |
|
||||
|
||||
### 4.4 Observability & Tracing
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| Micrometer | 1.16.1 | Metriken (Prometheus-Export) |
|
||||
| Micrometer Tracing | 1.6.1 | Distributed Tracing |
|
||||
| Zipkin Reporter | 3.5.1 | Trace-Export zu Zipkin |
|
||||
| Logback | 1.5.25 | Logging |
|
||||
|
||||
### 4.5 Security
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| Keycloak Admin Client | 26.0.7 | Keycloak-API-Integration |
|
||||
| Spring Security OAuth2 | (Boot) | JWT Resource Server |
|
||||
| Jackson (Kotlin) | 3.0.3 | JSON-Serialisierung |
|
||||
|
||||
### 4.6 API & Dokumentation
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| Springdoc OpenAPI | 3.0.0 | Swagger / OpenAPI 3.1 |
|
||||
| Jakarta Annotation | 3.0.0 | Jakarta EE Annotations |
|
||||
|
||||
### 4.7 Modul-Struktur
|
||||
|
||||
```
|
||||
backend/
|
||||
├── infrastructure/
|
||||
│ ├── gateway/ # ✅ API-Gateway (Spring Cloud Gateway)
|
||||
│ ├── security/ # ✅ Zentrales OAuth2/JWT-Modul (DRY)
|
||||
│ ├── cache/
|
||||
│ │ ├── cache-api/ # Interface-Abstraktion
|
||||
│ │ └── valkey-impl/ # Valkey-Implementierung
|
||||
│ ├── event-store/
|
||||
│ │ ├── event-store-api/ # Interface-Abstraktion
|
||||
│ │ └── valkey-impl/ # Valkey-Implementierung
|
||||
│ ├── persistence/ # Hybrid JPA + Exposed
|
||||
│ └── messaging/ # Kafka (Phase 3 / Outbox-Pattern)
|
||||
└── services/
|
||||
├── ping/ # ✅ Deployed — Test/Blueprint-Service
|
||||
├── entries/ # ⚙️ Registriert, noch nicht deployed
|
||||
├── events/ # 👻 Ghost Service (nicht registriert)
|
||||
├── horses/ # 👻 Ghost Service (nicht registriert)
|
||||
├── masterdata/ # 👻 Ghost Service (nicht registriert)
|
||||
├── registry/ # 👻 Ghost Service (nicht registriert)
|
||||
├── results/ # 👻 Ghost Service (nicht registriert)
|
||||
└── scheduling/ # 👻 Ghost Service (nicht registriert)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Infrastruktur-Services (Docker)
|
||||
|
||||
### 5.1 Laufende Services
|
||||
|
||||
| Service | Image / Version | Port(s) | Zweck |
|
||||
|:---------------|:--------------------------|:--------------|:-----------------------------|
|
||||
| PostgreSQL | postgres:16-alpine | 5432 | Primäre Datenbank |
|
||||
| Keycloak | keycloak:26.4 (custom) | 8180, 9000 | IAM / OAuth2 / OIDC |
|
||||
| Valkey | valkey:8-alpine | 6379 | Cache + Event-Store |
|
||||
| Consul | consul:1.21 | 8500, 8600 | Service Discovery + Config |
|
||||
| Zipkin | openzipkin/zipkin:3.5 | 9411 | Distributed Tracing |
|
||||
| Prometheus | prom/prometheus:v3.4 | 9090 | Metriken-Sammlung |
|
||||
| Grafana | grafana/grafana:11.6 | 3000 | Dashboards / Visualisierung |
|
||||
| Caddy | caddy:2.10-alpine | 4000 | Web-App Serving (Frontend) |
|
||||
| API-Gateway | (custom Spring Boot) | 8081 | Zentraler Eintrittspunkt |
|
||||
| Ping-Service | (custom Spring Boot) | 8082 | Test/Blueprint-Service |
|
||||
|
||||
### 5.2 CI/CD & DevOps
|
||||
|
||||
| Tool | Version / Details | Zweck |
|
||||
|:----------------|:--------------------------|:-----------------------------|
|
||||
| Gitea | Self-Hosted (CT 101) | Git-Repository + Registry |
|
||||
| Gitea Actions | (Runner VM 102) | CI/CD-Pipeline |
|
||||
| Docker Buildx | ARM64 (linux/arm64) | Multi-Arch Image Build |
|
||||
| Pangolin | Self-Hosted Tunnel | Reverse Proxy / Extern-Zugang|
|
||||
|
||||
### 5.3 Netzwerk & Routing
|
||||
|
||||
| Subdomain | Intern (Zora) | Zweck |
|
||||
|:-----------------------|:--------------------|:-------------------------|
|
||||
| `git.mo-code.at` | `10.0.0.22:3000` | Gitea |
|
||||
| `api.mo-code.at` | `10.0.0.50:8081` | API-Gateway |
|
||||
| `auth.mo-code.at` | `10.0.0.50:8180` | Keycloak |
|
||||
| `app.mo-code.at` | `10.0.0.50:4000` | Web-App |
|
||||
| `photos.mo-code.at` | `10.0.0.24:2283` | Immich (Fotos) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Code-Qualität & Build-Tools
|
||||
|
||||
| Tool | Version | Zweck |
|
||||
|:------------------|:----------|:-----------------------------------|
|
||||
| Detekt | 1.23.6 | Kotlin Static Analysis |
|
||||
| ktlint | 12.1.1 | Kotlin Code Formatter |
|
||||
| ArchUnit | 1.4.1 | Architektur-Tests (Layer-Regeln) |
|
||||
| Dokka | 2.1.0 | API-Dokumentation (KDoc) |
|
||||
| Ben-Manes Versions| 0.51.0 | Dependency-Update-Check |
|
||||
| KSP | 2.3.4 | Kotlin Symbol Processing |
|
||||
|
||||
### Build-Konfiguration
|
||||
|
||||
```properties
|
||||
# gradle.properties
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.workers.max=8
|
||||
org.gradle.configuration-cache=false # wegen JS-Test Serialisierungsproblem
|
||||
org.gradle.dependency.verification=strict
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Test-Stack
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:------------------------------|:----------|:-----------------------------------|
|
||||
| JUnit Jupiter | 5.11.3 | Unit-Tests |
|
||||
| JUnit Platform | 1.11.3 | Test-Runner |
|
||||
| MockK | 1.14.7 | Kotlin Mocking |
|
||||
| AssertJ | 3.27.7 | Fluent Assertions |
|
||||
| Testcontainers | 2.0.3 | Integration-Tests (Docker) |
|
||||
| Testcontainers Keycloak | 4.0.1 | Keycloak-Integration-Tests |
|
||||
| Testcontainers PostgreSQL | 1.21.4 | DB-Integration-Tests |
|
||||
| Testcontainers Kafka | 1.21.4 | Kafka-Integration-Tests |
|
||||
| ArchUnit | 1.4.1 | Architektur-Compliance-Tests |
|
||||
|
||||
---
|
||||
|
||||
## 8. Architektur-Prinzipien (ADRs)
|
||||
|
||||
| ADR | Entscheidung | Status |
|
||||
|:------|:--------------------------------------------------|:---------|
|
||||
| 0001 | Modulare Architektur (DDD, Clean Architecture) | ✅ Aktiv |
|
||||
| 0003 | Microservices-Architektur | ✅ Aktiv |
|
||||
| 0004 | Event-Driven Communication (Kafka Phase 3) | ✅ Aktiv |
|
||||
| 001 | Backend-Infrastruktur: Hybrid JPA+Exposed, Valkey | ✅ Aktiv |
|
||||
| 0013 | Tech-Stack-Stabilisierung 2026 (Versionen) | ✅ Aktiv |
|
||||
|
||||
**Kern-Prinzipien:**
|
||||
- **Offline-First:** SQLDelight als Cross-Platform-DB, Sync-Mechanismus
|
||||
- **Docs-as-Code:** `/docs` als Single Source of Truth
|
||||
- **DRY-Infrastruktur:** Shared Security/Cache/EventStore-Module
|
||||
- **ARM64-Native:** Alle Images für `linux/arm64` gebaut
|
||||
|
||||
---
|
||||
|
||||
## 9. Relevanz für Self-Hosted AI
|
||||
|
||||
### 9.1 Welche AI-Aufgaben entstehen im Projekt?
|
||||
|
||||
| Aufgabe | Häufigkeit | Komplexität |
|
||||
|:---------------------------------|:-----------|:------------|
|
||||
| Kotlin/Spring Boot Code-Completion| Täglich | Mittel |
|
||||
| Compose Multiplatform UI-Code | Täglich | Mittel |
|
||||
| SQL / Exposed DSL Queries | Häufig | Mittel |
|
||||
| Gradle Kotlin DSL Build-Skripte | Gelegentlich| Niedrig |
|
||||
| Docker / YAML Konfigurationen | Gelegentlich| Niedrig |
|
||||
| Architektur-Entscheidungen (ADR) | Selten | Hoch |
|
||||
| Fachlogik FEI/ÖTO Regelwerk | Selten | Sehr hoch |
|
||||
|
||||
### 9.2 Anforderungen an das AI-Modell
|
||||
|
||||
```
|
||||
MUSS:
|
||||
✅ Kotlin (JVM + Multiplatform) — sehr gute Unterstützung
|
||||
✅ Spring Boot / Spring Cloud — sehr gute Unterstützung
|
||||
✅ Compose Multiplatform — gute Unterstützung (neuere Modelle)
|
||||
✅ SQL / PostgreSQL — sehr gute Unterstützung
|
||||
✅ Docker / YAML — sehr gute Unterstützung
|
||||
✅ Deutsch (Fachsprache Reitsport) — für RAG-Dokumente
|
||||
|
||||
NICE-TO-HAVE:
|
||||
⭐ Gradle Kotlin DSL
|
||||
⭐ Keycloak / OAuth2 / OIDC
|
||||
⭐ Microservices-Architektur-Patterns
|
||||
```
|
||||
|
||||
### 9.3 Empfohlene Modelle für diesen Stack
|
||||
|
||||
| Modell | Größe | Stärke | RAM-Bedarf |
|
||||
|:------------------------|:-------|:------------------------------------|:-----------|
|
||||
| `qwen2.5-coder:14b` | 14B | Code (Kotlin, Java, SQL) — Top | ~10 GB |
|
||||
| `deepseek-coder-v2:16b` | 16B | Code-Completion, Refactoring | ~12 GB |
|
||||
| `llama3.1:8b` | 8B | Allgemein + Deutsch, schnell | ~6 GB |
|
||||
| `qwen2.5:32b` | 32B | Architektur, Planung, Fachlogik | ~22 GB |
|
||||
| `codellama:13b` | 13B | Code-Completion (IntelliJ-Plugin) | ~9 GB |
|
||||
|
||||
> **Empfehlung für Zora (64 GB RAM):**
|
||||
> - **Primär:** `qwen2.5-coder:14b` — bester Kotlin/Spring-Support
|
||||
> - **Sekundär:** `qwen2.5:32b` — für Architektur und Fachlogik
|
||||
> - **IntelliJ-Integration:** `codellama:13b` oder `qwen2.5-coder:14b`
|
||||
|
||||
### 9.4 RAG-Dokumente (Priorität)
|
||||
|
||||
Folgende Projekt-Dokumente sind besonders wertvoll als RAG-Kontext:
|
||||
|
||||
```
|
||||
Hohe Priorität:
|
||||
├── docs/01_Architecture/MASTER_ROADMAP_2026_Q1.md
|
||||
├── docs/01_Architecture/adr/ (alle ADRs)
|
||||
├── docs/04_Agents/Playbooks/ (Agenten-Rollen)
|
||||
├── gradle/libs.versions.toml (Versions-SSoT)
|
||||
└── docs/07_Infrastructure/ (Infrastruktur-Referenz)
|
||||
|
||||
Mittlere Priorität:
|
||||
├── docs/05_Backend/ (Backend-Guides)
|
||||
├── docs/06_Frontend/ (Frontend-Guides)
|
||||
└── docs/03_Domain/ (Fachlogik-Konzepte)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Zusammenfassung
|
||||
|
||||
```
|
||||
TECH-STACK KOMPLEXITÄT
|
||||
──────────────────────────────────────────────────────
|
||||
Sprachen: Kotlin (JVM + KMP/JS)
|
||||
Build: Gradle 9.3.1 + Kotlin DSL + libs.versions.toml
|
||||
Frontend: Compose Multiplatform 1.10 + SQLDelight 2.2 + Koin 4.1
|
||||
Backend: Spring Boot 3.5.9 + Spring Cloud 2025.0.1
|
||||
Persistenz: PostgreSQL 16 + Exposed 1.0 + Flyway 11 + HikariCP 7
|
||||
Cache: Valkey 8 + Lettuce 6.6 + Caffeine 3.2
|
||||
Security: Keycloak 26.4 + Spring Security OAuth2 + JWT
|
||||
Observability: Micrometer 1.16 + Zipkin 3.5 + Prometheus + Grafana 11.6
|
||||
Service Mesh: Consul 1.21 + Spring Cloud Gateway
|
||||
Messaging: Kafka (Phase 3) + Reactor Kafka 1.3
|
||||
CI/CD: Gitea Actions + Docker Buildx (ARM64)
|
||||
Hosting: Proxmox VE 8.4 + Docker Compose + Pangolin-Tunnel
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
date: 2026-04-21
|
||||
---
|
||||
|
||||
# Wizard‑DSL & Orchestrator – Referenz
|
||||
|
||||
## Ziel
|
||||
Deklarative Beschreibung von Wizard‑Flows als Graph (Steps, Guards, Transitions) mit klaren Side‑Effects und Offline‑Draft‑Unterstützung.
|
||||
|
||||
## Kern‑Interfaces (Skizze)
|
||||
```kotlin
|
||||
interface StepId
|
||||
|
||||
data class WizardContext(
|
||||
val origin: AppScreen,
|
||||
val role: String?,
|
||||
val isOnline: Boolean,
|
||||
val stats: MasterdataStats?
|
||||
)
|
||||
|
||||
data class WizardState<S: StepId, A>(
|
||||
val current: S,
|
||||
val history: List<S> = emptyList(),
|
||||
val acc: A,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
typealias Guard<S, A> = (WizardContext, A) -> Boolean
|
||||
|
||||
data class Transition<S: StepId>(val target: S, val guard: Guard<S, *>? = null, val id: String)
|
||||
|
||||
interface StepEffects<S: StepId, A> {
|
||||
suspend fun onEnter(ctx: WizardContext, state: WizardState<S, A>) {}
|
||||
suspend fun onLeave(ctx: WizardContext, state: WizardState<S, A>) {}
|
||||
suspend fun onComplete(ctx: WizardContext, state: WizardState<S, A>) {}
|
||||
}
|
||||
```
|
||||
|
||||
## DSL (Skizze)
|
||||
```kotlin
|
||||
class FlowBuilder<S: StepId, A> {
|
||||
fun step(id: S, block: StepBuilder<S, A>.() -> Unit) { /* … */ }
|
||||
}
|
||||
class StepBuilder<S: StepId, A> {
|
||||
fun onEnter(block: suspend (WizardContext, WizardState<S, A>) -> Unit) { /* … */ }
|
||||
fun whenGuard(id: String, g: Guard<S, A>, go: S) { /* … */ }
|
||||
fun otherwise(go: S) { /* … */ }
|
||||
}
|
||||
fun <S: StepId, A> flow(start: S, build: FlowBuilder<S, A>.() -> Unit): WizardRuntime<S, A> { /* … */ }
|
||||
```
|
||||
|
||||
## Beispiel – Event‑Flow (Auszug)
|
||||
```kotlin
|
||||
sealed interface EventStep: StepId {
|
||||
data object ZnsCheck: EventStep
|
||||
data object VeranstalterSelection: EventStep
|
||||
data object AnsprechpersonMapping: EventStep
|
||||
data object MetaData: EventStep
|
||||
data object TurnierAnlage: EventStep
|
||||
data object Summary: EventStep
|
||||
}
|
||||
|
||||
data class EventAcc(
|
||||
val veranstalterId: Uuid? = null,
|
||||
val veranstalterNr: String = "",
|
||||
val veranstalterName: String = "",
|
||||
val ansprechpersonSatznr: String = "",
|
||||
val name: String = "",
|
||||
val ort: String = "",
|
||||
val start: LocalDate? = null,
|
||||
val end: LocalDate? = null,
|
||||
val turniere: List<TurnierEntry> = emptyList()
|
||||
)
|
||||
|
||||
object EventGuards {
|
||||
val hasZns: Guard<EventStep, EventAcc> = { ctx, _ -> (ctx.stats?.vereinCount ?: 0) > 0 }
|
||||
val needsContactPerson: Guard<EventStep, EventAcc> = { _, acc -> acc.veranstalterId == null || acc.veranstalterNr.startsWith("ORG-") }
|
||||
}
|
||||
|
||||
val EventFlow = flow<EventStep, EventAcc>(start = EventStep.ZnsCheck) {
|
||||
step(EventStep.ZnsCheck) {
|
||||
onEnter { ctx, _ -> /* prefetch stats */ }
|
||||
whenGuard("hasZns", EventGuards.hasZns, go = EventStep.VeranstalterSelection)
|
||||
otherwise(go = EventStep.VeranstalterSelection)
|
||||
}
|
||||
step(EventStep.VeranstalterSelection) {
|
||||
whenGuard("needsContact", EventGuards.needsContactPerson, go = EventStep.AnsprechpersonMapping)
|
||||
otherwise(go = EventStep.MetaData)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DevTools
|
||||
- Strukturierte Logs je Transition (from, to, guard-id, result, duration).
|
||||
- Graph‑Export (DOT/PlantUML) aus der DSL für Doku & Reviews.
|
||||
|
||||
## Tests (Empfehlungen)
|
||||
- Unit: Guards (100% Branch‑Abdeckung), Runtime‑History.
|
||||
- Property: Resume‑Determinismus (Draft → korrekter Step).
|
||||
- Snapshot: Compose‑Panels mit Beispielkontexten.
|
||||
|
||||
## Verweise
|
||||
- ADR‑0025 Orchestrator · ADR‑0026 Validation‑Policy · ADR‑0027 Draft‑Domain & Delta‑Sync
|
||||
- Roadmap: `docs/01_Architecture/MASTER_ROADMAP.md#3-initiative-wizard-orchestrator--offline-drafts-q2q3-2026`
|
||||
+8
-1
@@ -3,22 +3,26 @@
|
||||
## 🏗️ [Lead Architect] | 14. April 2026
|
||||
|
||||
Dieses Dokument beschreibt die Umsetzung der Online-Nennung für das Turnier in Neumarkt (24. April 2026).
|
||||
Ziel ist ein schlankes Web-Formular, das strukturierte E-Mails an den `Mail-Service` sendet, welcher diese verarbeitet und in der Desktop-Zentrale zur manuellen Übernahme bereitstellt.
|
||||
Ziel ist ein schlankes Web-Formular, das strukturierte E-Mails an den `Mail-Service` sendet, welcher diese verarbeitet
|
||||
und in der Desktop-Zentrale zur manuellen Übernahme bereitstellt.
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: E-Mail-Infrastruktur (Vorbereitung) ✅
|
||||
|
||||
* [x] Definition des Adress-Schemas: `meldestelle-[Turnier-Nr]@mo-code.at`.
|
||||
* [x] Konfiguration der World4You SMTP/IMAP Zugangsdaten.
|
||||
* [x] Mailpit Integration für lokale Tests (bereits in `dc-ops.yaml`).
|
||||
|
||||
### Phase 2: Das Web-Formular (WasmJS Frontend) ✅
|
||||
|
||||
* [x] **Basis-UI:** Erstellung des Formulars gemäß Spezifikation (Reiter, Pferd, Lizenz, Bewerbe).
|
||||
* [x] **Validierung:** Implementierung der Pflichtfeld-Prüfung (Buttonsperre bis alles ok).
|
||||
* [x] **Mail-Versand:** Integration des API-Calls an das Backend (`mail-service`), um die Nennung zu speichern.
|
||||
* [x] **DSGVO:** Checkbox und Hinweistext eingebaut.
|
||||
|
||||
### Phase 3: Mail-Service (Backend-Verarbeitung) 🏗️
|
||||
|
||||
* [x] **Endpoint:** POST-Endpunkt für direkte Nennungen aus dem Web-Formular implementiert.
|
||||
* [ ] **Polling:** Implementierung des IMAP-Pollers (imap.world4you.com).
|
||||
* [ ] **Parsing:** Extraktion der Turnier-Nummer aus dem `To`-Header und Mapping auf das Datenbank-Schema (Tenant).
|
||||
@@ -26,12 +30,14 @@ Ziel ist ein schlankes Web-Formular, das strukturierte E-Mails an den `Mail-Serv
|
||||
* [x] **Persistence:** Speichern der eingegangenen "Nennungs-Mails" in einer temporären Tabelle.
|
||||
|
||||
### Phase 4: Desktop-Zentrale Integration ✅
|
||||
|
||||
* [x] **UI-Tab:** Neuer Reiter "Online-Eingang" in der Turnierverwaltung (`TurnierDetailScreen`).
|
||||
* [x] **Vorschau:** Anzeige der eingegangenen Nennungen mit Details (`OnlineNennungEingangTabContent`).
|
||||
* [x] **Übernahme:** "Übernehmen"-Button, der Reiter/Pferd in die Nennung vorausfüllt (`NennungViewModel`).
|
||||
* [ ] **Abschluss:** Manueller "Bestätigen"-Button zum Versenden der finalen Bestätigungsmail.
|
||||
|
||||
### Phase 5: End-to-End Test & Deployment 🚀 (Deadline: 21.04.2026)
|
||||
|
||||
* [ ] Test-Nennung über Web-Formular (Mailpit).
|
||||
* [ ] Verifikation der Schema-Zuordnung im Backend.
|
||||
* [ ] Live-Test mit `online-nennen@mo-code.at`.
|
||||
@@ -40,6 +46,7 @@ Ziel ist ein schlankes Web-Formular, das strukturierte E-Mails an den `Mail-Serv
|
||||
---
|
||||
|
||||
### Meilensteine
|
||||
|
||||
1. **16.04.:** Web-Formular ist funktionsfähig (Senden möglich).
|
||||
2. **18.04.:** Mail-Service verarbeitet Mails und sendet Auto-Antworten.
|
||||
3. **20.04.:** Desktop-UI zur Übernahme ist fertig.
|
||||
+5
-2
@@ -10,7 +10,9 @@ last_update: 2026-03-25
|
||||
10. **🏗️ [Lead Architect]** | 5. April 2026
|
||||
|
||||
**Kontext:**
|
||||
Der ZNS-Importer wurde für die Verarbeitung der Datei `RICHT01.dat` optimiert. Es wurde klargestellt, dass Richter (SatzID 'X') und Parcoursbauer (SatzID 'Y') gemeinsam in dieser Datei geliefert werden. Eine separate `PARCO01.dat` existiert nicht.
|
||||
Der ZNS-Importer wurde für die Verarbeitung der Datei `RICHT01.dat` optimiert. Es wurde klargestellt, dass Richter (
|
||||
SatzID 'X') und Parcoursbauer (SatzID 'Y') gemeinsam in dieser Datei geliefert werden. Eine separate `PARCO01.dat`
|
||||
existiert nicht.
|
||||
Um den `registration-context` und `actor-context` unter realistischen Bedingungen testen zu können, benötigen wir echte
|
||||
Stammdaten (Reiter, Pferde, Vereine, Funktionäre). Diese Daten werden vom ÖPS (Österreichischer Pferdesportverband) in
|
||||
Form einer `ZNS.zip` bereitgestellt.
|
||||
@@ -51,7 +53,8 @@ gesteuert wird und die Daten persistent im Backend (`actor-context`) ablegt.
|
||||
* ZIP-Entpackung in-memory implementiert (`ZnsImportService`).
|
||||
* [x] **Legacy-Parser (CP850 Fixed-Width):**
|
||||
* `ZnsLegacyParsers` in `core:zns-parser` (KMP-Modul) implementiert.
|
||||
* Alle 4 Dateitypen (VEREIN01, LIZENZ01, PFERDE01, RICHT01) bytegenau gemappt. RICHT01 verarbeitet Richter ('X') und Parcoursbauer ('Y'). ✅
|
||||
* Alle 4 Dateitypen (VEREIN01, LIZENZ01, PFERDE01, RICHT01) bytegenau gemappt. RICHT01 verarbeitet Richter ('X') und
|
||||
Parcoursbauer ('Y'). ✅
|
||||
|
||||
### Phase 2: Domain-Mapping & Persistenz (👷 Backend Developer)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-03-15
|
||||
---
|
||||
|
||||
# Frontend-Architektur & Modularisierungsstrategie
|
||||
|
||||
**Status:** ENTWURF
|
||||
**Zuletzt aktualisiert:** 2026-01-19
|
||||
**Kontext:** Migration zu Clean Architecture & Feature-Modulen
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht
|
||||
|
||||
Die Frontend-Architektur von **Meldestelle** basiert auf **Kotlin Multiplatform (KMP)** mit **Compose Multiplatform**
|
||||
für die Benutzeroberfläche. Wir folgen einem strikten **Clean Architecture**-Ansatz, um Testbarkeit, Skalierbarkeit und
|
||||
Trennung der Zuständigkeiten sicherzustellen.
|
||||
|
||||
## 2. Modulstruktur
|
||||
|
||||
Das Projekt ist in folgende Schichten unterteilt:
|
||||
|
||||
### 2.1 Core-Module (`frontend/core`)
|
||||
|
||||
Wiederverwendbare Komponenten, die unabhängig von spezifischen Geschäftsfunktionen sind.
|
||||
|
||||
* `core-network`: Zentrale HTTP-Client-Konfiguration (Auth, Logging, ContentNegotiation).
|
||||
* `core-sync`: Generische Synchronisierungslogik (`SyncManager`, `SyncableRepository`).
|
||||
* `core-ui`: Gemeinsame UI-Komponenten und Design-System.
|
||||
|
||||
### 2.2 Feature-Module (`frontend/features`)
|
||||
|
||||
Jede Geschäftsdomäne (z.B. `ping`, `auth`, `events`) liegt in ihrem eigenen Modul.
|
||||
Ein Feature-Modul MUSS die **Clean Architecture** Paketstruktur einhalten:
|
||||
|
||||
* `at.mocode.{feature}.feature.domain`
|
||||
* **Entitäten:** Reine Datenklassen.
|
||||
* **Interfaces:** Repository-Interfaces, Service-Interfaces.
|
||||
* **Use Cases:** Geschäftslogik (optional, für komplexe Logik).
|
||||
* `at.mocode.{feature}.feature.data`
|
||||
* **Implementierungen:** Repository-Implementierungen, API-Clients.
|
||||
* **DTOs:** Data Transfer Objects (wenn von Domain-Entitäten abweichend).
|
||||
* `at.mocode.{feature}.feature.presentation`
|
||||
* **ViewModels:** Zustandsverwaltung.
|
||||
* **Screens:** Composable-Funktionen.
|
||||
* `at.mocode.{feature}.feature.di`
|
||||
* **Koin-Modul:** Konfiguration der Dependency Injection.
|
||||
|
||||
### 2.3 Shells (`frontend/shells`)
|
||||
|
||||
Anwendungs-Einstiegspunkte, die alles zusammenführen.
|
||||
|
||||
* `meldestelle-portal`: Die Haupt-Web-/Desktop-Anwendung.
|
||||
|
||||
## 3. Migrationsstrategie (Übergangsphase)
|
||||
|
||||
Wir migrieren aktuell von einer monolithischen `clients`-Paketstruktur zu modularen Feature-Modulen.
|
||||
|
||||
**Regeln für die Migration:**
|
||||
|
||||
1. **Neue Features:** Müssen direkt in `frontend/features/{name}` unter Verwendung der Clean Architecture-Struktur
|
||||
implementiert werden.
|
||||
2. **Bestehende Features:** Werden schrittweise migriert.
|
||||
3. **Koexistenz:** Während des Übergangs ist Legacy-Code in `clients/` erlaubt, aber als veraltet markiert.
|
||||
4. **Dependency Injection:** Legacy-Code muss die neuen Koin-Module verwenden, sofern verfügbar.
|
||||
5. **Keine Ghost-Klassen:** Klassen nicht duplizieren. Wenn eine Klasse in ein Feature-Modul verschoben wird, muss die
|
||||
alte in `clients/` gelöscht werden.
|
||||
|
||||
## 4. Wichtige Entscheidungen (ADRs)
|
||||
|
||||
### ADR-001: Entkopplung der Sync-Logik
|
||||
|
||||
* **Entscheidung:** ViewModels dürfen nicht direkt vom `SyncManager` abhängen.
|
||||
* **Begründung:** Um einfacheres Testen zu ermöglichen und die Komplexität des generischen Sync-Mechanismus zu
|
||||
verbergen.
|
||||
* **Umsetzung:** Ein Domain-Service-Interface (z.B. `PingSyncService`) einführen, das den `SyncManager`-Aufruf kapselt.
|
||||
|
||||
### ADR-002: Feature-Modul-Isolation
|
||||
|
||||
* **Entscheidung:** Feature-Module sollten nach Möglichkeit nicht direkt voneinander abhängen.
|
||||
* **Kommunikation:** Gemeinsame Core-Module oder lose Kopplung über Interfaces/Events verwenden, wenn modulübergreifende
|
||||
Kommunikation nötig ist.
|
||||
|
||||
---
|
||||
|
||||
**Freigegeben von:** Lead Architect
|
||||
+19
-9
@@ -3,11 +3,15 @@ type: Reference
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
---
|
||||
|
||||
# Architektur: Das Platform-Modul
|
||||
|
||||
## Überblick
|
||||
|
||||
Das **Platform-Modul** ist das Rückgrat der Build-Infrastruktur des Meldestelle-Projekts. Seine alleinige Aufgabe ist die zentrale Verwaltung und Bereitstellung von Abhängigkeiten und deren Versionen. Dies stellt sicher, dass alle Module im gesamten Projekt dieselben Bibliotheksversionen verwenden, was Inkonsistenzen ("JAR Hell") verhindert und die Wartbarkeit drastisch verbessert.
|
||||
Das **Platform-Modul** ist das Rückgrat der Build-Infrastruktur des Meldestelle-Projekts. Seine alleinige Aufgabe ist
|
||||
die zentrale Verwaltung und Bereitstellung von Abhängigkeiten und deren Versionen. Dies stellt sicher, dass alle Module
|
||||
im gesamten Projekt dieselben Bibliotheksversionen verwenden, was Inkonsistenzen ("JAR Hell") verhindert und die
|
||||
Wartbarkeit drastisch verbessert.
|
||||
|
||||
Das Modul agiert als eine interne "Single Source of Truth" für alle externen Bibliotheken.
|
||||
|
||||
@@ -24,19 +28,23 @@ platform/
|
||||
|
||||
### `platform-bom`
|
||||
|
||||
Dies ist das wichtigste Modul der Plattform. Es ist als "Bill of Materials" (BOM) konfiguriert und nutzt das `java-platform`-Plugin von Gradle.
|
||||
Dies ist das wichtigste Modul der Plattform. Es ist als "Bill of Materials" (BOM) konfiguriert und nutzt das
|
||||
`java-platform`-Plugin von Gradle.
|
||||
|
||||
* **Zweck:** Definiert eine umfassende Liste von Abhängigkeiten und deren exakten, geprüften Versionen. Es importiert auch andere wichtige BOMs (z.B. von Spring Boot und Kotlin).
|
||||
* **Funktionsweise:** Andere Module importieren diese BOM mit `platform(projects.platform.platformBom)`. Gradle sorgt dann dafür, dass alle transitiven und deklarierten Abhängigkeiten den in der BOM festgelegten Versionen entsprechen.
|
||||
* **Zweck:** Definiert eine umfassende Liste von Abhängigkeiten und deren exakten, geprüften Versionen. Es importiert
|
||||
auch andere wichtige BOMs (z.B. von Spring Boot und Kotlin).
|
||||
* **Funktionsweise:** Andere Module importieren diese BOM mit `platform(projects.platform.platformBom)`. Gradle sorgt
|
||||
dann dafür, dass alle transitiven und deklarierten Abhängigkeiten den in der BOM festgelegten Versionen entsprechen.
|
||||
* **Vorteil:** Absolute Versionskontrolle über das gesamte Projekt.
|
||||
|
||||
### `platform-dependencies`
|
||||
|
||||
Ein einfaches "Sammelmodul", das die am häufigsten benötigten Laufzeit-Abhängigkeiten bündelt.
|
||||
|
||||
* **Zweck:** Vereinfacht die `build.gradle.kts`-Dateien der Service-Module. Anstatt 5-6 einzelne `kotlinx`- und Logging-Bibliotheken hinzuzufügen, genügt eine einzige Abhängigkeit zu diesem Modul.
|
||||
* **Zweck:** Vereinfacht die `build.gradle.kts`-Dateien der Service-Module. Anstatt 5-6 einzelne `kotlinx`- und
|
||||
Logging-Bibliotheken hinzuzufügen, genügt eine einzige Abhängigkeit zu diesem Modul.
|
||||
* **Verwendung:**
|
||||
|
||||
|
||||
```kotlin
|
||||
// In einem Service-Modul
|
||||
dependencies {
|
||||
@@ -48,9 +56,10 @@ Ein einfaches "Sammelmodul", das die am häufigsten benötigten Laufzeit-Abhäng
|
||||
|
||||
Analog zu `platform-dependencies`, aber speziell für Test-Bibliotheken.
|
||||
|
||||
* **Zweck:** Stellt ein konsistentes Set an Test-Frameworks (JUnit 5, MockK, AssertJ) und Werkzeugen (Testcontainers) für alle Module bereit.
|
||||
* **Zweck:** Stellt ein konsistentes Set an Test-Frameworks (JUnit 5, MockK, AssertJ) und Werkzeugen (Testcontainers)
|
||||
für alle Module bereit.
|
||||
* **Verwendung:**
|
||||
|
||||
|
||||
```kotlin
|
||||
// In einem Service-Modul
|
||||
dependencies {
|
||||
@@ -58,4 +67,5 @@ Analog zu `platform-dependencies`, aber speziell für Test-Bibliotheken.
|
||||
}
|
||||
```
|
||||
|
||||
* **Optimierung:** Dieses Modul nutzt die in `libs.versions.toml` definierten `[bundles]`, um die Build-Datei extrem kurz und lesbar zu halten.
|
||||
* **Optimierung:** Dieses Modul nutzt die in `libs.versions.toml` definierten `[bundles]`, um die Build-Datei extrem
|
||||
kurz und lesbar zu halten.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user