Compare commits
203 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 | |||
| 88983f2b4e | |||
| 8f6044abe3 | |||
| 8857d52f44 | |||
| 3949ab21db | |||
| 0128f98164 | |||
| 4b6a242372 | |||
| a1194adeac | |||
| 26b3b193ca | |||
| dd76ad6d14 | |||
| cfc412878f | |||
| 0426d4ee9a | |||
| 8f45544fe1 | |||
| edd33c34dc | |||
| b8bd2744ac | |||
| b2e6c2427b | |||
| 3b7abc55a4 | |||
| 29c35c524b | |||
| f3d5651ab7 | |||
| ba812e230d | |||
| cb4f2f855c | |||
| 10f9e82718 | |||
| eb0fac5989 | |||
| 82a4a13505 |
@@ -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/
|
||||
@@ -1,13 +1,17 @@
|
||||
# 🤖 Projekt Agenten & Protokoll (Meldestelle-Biest)
|
||||
# 🤖 Projekt Agenten & Protokoll (Meldestelle)
|
||||
|
||||
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
|
||||
Das Projekt **"Meldestelle-Biest"** entwickelt eine ÖTO/FEI-konforme, offline-fähige Turnier-Software.
|
||||
## 🚀 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.
|
||||
@@ -29,14 +33,16 @@ Jede Agenten-Antwort **muss** mit dem entsprechenden Badge beginnen, um den Kont
|
||||
* **🧹 [Curator]**: Wissens-Management & Dokumentations-Check (ADR, Reference, Journal). Beendet jede Session.
|
||||
* [Playbook](docs/04_Agents/Playbooks/Curator.md)
|
||||
|
||||
## 2. Der "Biest"-Workflow
|
||||
## 2. Der "Meldestelle"-Workflow
|
||||
1. **Kontext-Check:** Lies immer zuerst die `MASTER_ROADMAP` in `docs/01_Architecture/`.
|
||||
2. **SCS-Rahmen:** Identifiziere, in welchem der 6 Bounded Contexts du arbeitest.
|
||||
3. **Fokus:** Bearbeite immer nur EINE fachliche Aufgabe pro Session.
|
||||
4. **Doku-as-Code:** Änderungen an Code/Architektur müssen sofort in `docs/` (ADR/Reference) reflektiert werden.
|
||||
5. **Session-Abschluss:** Jede Session endet mit einem Eintrag durch den **Curator** (Journal oder Artefakt).
|
||||
|
||||
## 3. Projekt-Philosophie
|
||||
* **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.
|
||||
|
||||
@@ -14,7 +14,7 @@ Die gesamte Projektdokumentation (Architektur, Fachdomäne, Entwickler-Anleitung
|
||||
**Starte hier:** [**→ docs/README.md**](./docs/README.md)
|
||||
|
||||
| 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 |
|
||||
|
||||
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()
|
||||
+19
-14
@@ -1,25 +1,30 @@
|
||||
package at.mocode.infrastructure.gateway
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication
|
||||
class GatewayApplication
|
||||
class GatewayApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(GatewayApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8081")
|
||||
val appName = env.getProperty("spring.application.name", "gateway")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val context = runApplication<GatewayApplication>(*args)
|
||||
val logger = LoggerFactory.getLogger(GatewayApplication::class.java)
|
||||
val env = context.getBean<Environment>()
|
||||
val port = env.getProperty("server.port") ?: "8081"
|
||||
|
||||
logger.info("""
|
||||
----------------------------------------------------------
|
||||
Application 'Gateway' is running!
|
||||
Port: $port
|
||||
Profiles: ${env.activeProfiles.joinToString(", ").ifEmpty { "default" }}
|
||||
----------------------------------------------------------
|
||||
""".trimIndent())
|
||||
runApplication<GatewayApplication>(*args)
|
||||
}
|
||||
|
||||
+8
-17
@@ -1,6 +1,5 @@
|
||||
package at.mocode.infrastructure.gateway.config
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.cloud.gateway.route.RouteLocator
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
|
||||
import org.springframework.cloud.gateway.route.builder.filters
|
||||
@@ -9,15 +8,7 @@ import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class GatewayConfig(
|
||||
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String,
|
||||
@Value("\${masterdata.service.url:http://localhost:8086}") private val masterdataServiceUrl: String,
|
||||
@Value("\${events.service.url:http://localhost:8085}") private val eventsServiceUrl: String,
|
||||
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String,
|
||||
@Value("\${results.service.url:http://localhost:8088}") private val resultsServiceUrl: String,
|
||||
@Value("\${series.service.url:http://localhost:8089}") private val seriesServiceUrl: String,
|
||||
@Value("\${billing.service.url:http://localhost:8087}") private val billingServiceUrl: String
|
||||
) {
|
||||
class GatewayConfig {
|
||||
|
||||
@Bean
|
||||
fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
|
||||
@@ -31,31 +22,31 @@ class GatewayConfig(
|
||||
it.fallbackUri = java.net.URI.create("forward:/fallback/ping")
|
||||
}
|
||||
}
|
||||
uri(pingServiceUrl)
|
||||
uri("lb://ping-service")
|
||||
}
|
||||
route(id = "masterdata-service") {
|
||||
path("/api/v1/masterdata/**")
|
||||
uri(masterdataServiceUrl)
|
||||
uri("lb://masterdata-service")
|
||||
}
|
||||
route(id = "events-service") {
|
||||
path("/api/v1/events/**")
|
||||
uri(eventsServiceUrl)
|
||||
uri("lb://events-service")
|
||||
}
|
||||
route(id = "zns-import-service") {
|
||||
path("/api/v1/import/zns/**", "/api/v1/import/zns")
|
||||
uri(znsImportServiceUrl)
|
||||
uri("lb://zns-import-service")
|
||||
}
|
||||
route(id = "results-service") {
|
||||
path("/api/v1/results/**")
|
||||
uri(resultsServiceUrl)
|
||||
uri("lb://results-service")
|
||||
}
|
||||
route(id = "series-service") {
|
||||
path("/api/v1/series/**")
|
||||
uri(seriesServiceUrl)
|
||||
uri("lb://series-service")
|
||||
}
|
||||
route(id = "billing-service") {
|
||||
path("/api/v1/billing/**")
|
||||
uri(billingServiceUrl)
|
||||
uri("lb://billing-service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
@@ -20,14 +20,18 @@ spring:
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8081
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
# Bei lokalem Start (Gradle) wollen wir nicht die Docker-IP registrieren, sondern localhost oder die Host-IP.
|
||||
# Aber für den Anfang reicht es, wenn wir Consul finden.
|
||||
|
||||
gateway:
|
||||
httpclient: { }
|
||||
# Routen sind in GatewayConfig.kt definiert
|
||||
# Routen sind in GatewayConfig.kt via Service-Discovery (lb://) definiert
|
||||
|
||||
# --- SECURITY (OAuth2 Resource Server) ---
|
||||
security:
|
||||
@@ -40,6 +44,27 @@ spring:
|
||||
issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI:http://localhost:8180/realms/meldestelle}
|
||||
jwk-set-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI:http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs}
|
||||
|
||||
gateway:
|
||||
security:
|
||||
cors:
|
||||
allowed-origin-patterns:
|
||||
- "http://localhost:*"
|
||||
- "https://*.meldestelle.at"
|
||||
- "https://*.mo-code.at"
|
||||
- "https://*.postman.co"
|
||||
- "postman://*"
|
||||
allowed-methods:
|
||||
- "GET"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "DELETE"
|
||||
- "OPTIONS"
|
||||
- "PATCH"
|
||||
allowed-headers:
|
||||
- "*"
|
||||
allow-credentials: true
|
||||
max-age: 3600s
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
@@ -62,9 +87,3 @@ management:
|
||||
# Lokal: Zipkin auf Port 9411. In Docker via ENV MANAGEMENT_ZIPKIN_TRACING_ENDPOINT überschrieben.
|
||||
endpoint: ${MANAGEMENT_ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
|
||||
|
||||
# --- Custom Service URLs ---
|
||||
# Default: Localhost (für Entwicklung ohne Docker)
|
||||
# Im Docker-Compose überschreiben wir das mit dem Service-Namen
|
||||
ping:
|
||||
service:
|
||||
url: ${PING_SERVICE_URL:http://localhost:8082}
|
||||
|
||||
+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
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package at.mocode.zns.importer
|
||||
|
||||
/**
|
||||
* Der Modus des ZNS-Imports.
|
||||
*
|
||||
* [FULL] - Alle Dateien (Vereine, Reiter, Pferde, Funktionäre) werden importiert.
|
||||
* [LIGHT] - Nur Stammdaten (Vereine, Reiter) werden importiert (Performance-Optimiert).
|
||||
*/
|
||||
enum class ZnsImportMode {
|
||||
FULL,
|
||||
LIGHT
|
||||
}
|
||||
+156
-26
@@ -22,7 +22,7 @@ import java.util.zip.ZipInputStream
|
||||
* 1. VEREIN01.DAT → Verein (via VereinRepository)
|
||||
* 2. LIZENZ01.DAT → Reiter (via ReiterRepository)
|
||||
* 3. PFERDE01.DAT → Pferd (via HorseRepository)
|
||||
* 4. RICHT01.DAT → Funktionaer (via FunktionaerRepository)
|
||||
* 4. RICHT01.DAT → Funktionär (via FunktionaerRepository)
|
||||
*
|
||||
* Dieser Service hat **keine** Spring-Abhängigkeit und kann daher sowohl
|
||||
* im Backend (REST-Upload) als auch in der Compose Desktop App (Offline-Import)
|
||||
@@ -47,14 +47,15 @@ class ZnsImportService(
|
||||
companion object {
|
||||
private val CP850 = Charset.forName("Cp850")
|
||||
|
||||
private const val FILE_VEREIN = "VEREIN01.DAT"
|
||||
private const val FILE_LIZENZ = "LIZENZ01.DAT"
|
||||
private const val FILE_PFERDE = "PFERDE01.DAT"
|
||||
private const val FILE_RICHT = "RICHT01.DAT"
|
||||
private const val FILE_VEREIN = "VEREIN"
|
||||
private const val FILE_LIZENZ = "LIZENZ"
|
||||
private const val FILE_PFERDE = "PFERDE"
|
||||
private const val FILE_RICHT = "RICHT"
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert die relevanten Dateien aus dem ZIP-Archiv.
|
||||
* Optimiert: Nutzt BufferedReader für zeilenweises Einlesen, ohne das gesamte File in den RAM zu laden.
|
||||
*/
|
||||
fun extrahiereDateien(zipInputStream: InputStream): Map<String, List<String>> {
|
||||
val dateien = mutableMapOf<String, List<String>>()
|
||||
@@ -64,47 +65,168 @@ class ZnsImportService(
|
||||
while (entry != null) {
|
||||
val fileName = entry.name.uppercase().substringAfterLast("/")
|
||||
|
||||
if (fileName in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
|
||||
val outputStream = java.io.ByteArrayOutputStream()
|
||||
val buffer = ByteArray(4096)
|
||||
var len: Int
|
||||
while (zip.read(buffer).also { len = it } > 0) {
|
||||
outputStream.write(buffer, 0, len)
|
||||
// Toleranter Check: Erkennt VEREIN01.DAT, VEREIN.DAT, etc.
|
||||
val targetKey = when {
|
||||
fileName.startsWith(FILE_VEREIN) -> FILE_VEREIN
|
||||
fileName.startsWith(FILE_LIZENZ) -> FILE_LIZENZ
|
||||
fileName.startsWith(FILE_PFERDE) -> FILE_PFERDE
|
||||
fileName.startsWith(FILE_RICHT) -> FILE_RICHT
|
||||
else -> null
|
||||
}
|
||||
val content = outputStream.toString(CP850)
|
||||
val lines = content.split(Regex("\\r?\\n|\\r")).filter { it.isNotBlank() }
|
||||
dateien[fileName] = lines
|
||||
|
||||
if (targetKey != null && fileName.endsWith(".DAT")) {
|
||||
// Wir lesen den Stream direkt zeilenweise mit dem korrekten Encoding
|
||||
val lines = mutableListOf<String>()
|
||||
val reader = zip.bufferedReader(CP850)
|
||||
|
||||
// WICHTIG: Wir dürfen den Reader NICHT schließen (use), da sonst der ZipInputStream geschlossen wird!
|
||||
var line = reader.readLine()
|
||||
while (line != null) {
|
||||
if (line.isNotBlank()) {
|
||||
lines.add(line)
|
||||
}
|
||||
line = reader.readLine()
|
||||
}
|
||||
println("[DEBUG_LOG] Datei $fileName extrahiert als $targetKey: ${lines.size} Zeilen")
|
||||
dateien[targetKey] = lines
|
||||
}
|
||||
zip.closeEntry()
|
||||
entry = zip.nextEntry
|
||||
}
|
||||
} finally {
|
||||
// Wir schließen den ZipInputStream NICHT mit use,
|
||||
// um den zugrunde liegenden zipInputStream nicht vorzeitig zu schließen.
|
||||
// Falls der Aufrufer den Stream schließen will, soll er das tun.
|
||||
// Aber wir müssen sicherstellen, dass wir alle Entries gelesen haben.
|
||||
} catch (e: Exception) {
|
||||
println("[DEBUG_LOG] Fehler beim Extrahieren der ZIP (eventuell keine ZIP-Datei?): ${e.message}")
|
||||
}
|
||||
return dateien
|
||||
}
|
||||
|
||||
/**
|
||||
* Importiert ZNS-Daten aus einem Stream. Erkennt automatisch, ob es eine ZIP oder eine DAT ist.
|
||||
*/
|
||||
suspend fun importiereStream(
|
||||
inputStream: InputStream,
|
||||
fileName: String,
|
||||
mode: ZnsImportMode = ZnsImportMode.FULL
|
||||
): ZnsImportResult {
|
||||
val upperName = fileName.uppercase()
|
||||
return if (upperName.endsWith(".ZIP")) {
|
||||
importiereZip(inputStream, mode)
|
||||
} else if (upperName.endsWith(".DAT")) {
|
||||
importiereEinzelDatei(inputStream, upperName, mode)
|
||||
} else {
|
||||
ZnsImportResult(fehler = listOf("Dateiformat nicht unterstützt: $fileName"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importiereEinzelDatei(
|
||||
inputStream: InputStream,
|
||||
fileName: String,
|
||||
mode: ZnsImportMode
|
||||
): ZnsImportResult {
|
||||
println("[DEBUG_LOG] Importiere Einzeldatei: $fileName")
|
||||
val lines = inputStream.bufferedReader(CP850).readLines().filter { it.isNotBlank() }
|
||||
println("[DEBUG_LOG] Einzeldatei $fileName hat ${lines.size} Zeilen")
|
||||
|
||||
val fehler = mutableListOf<String>()
|
||||
val warnungen = mutableListOf<String>()
|
||||
|
||||
var vereineImportiert = 0
|
||||
var vereineAktualisiert = 0
|
||||
var reiterImportiert = 0
|
||||
var reiterAktualisiert = 0
|
||||
var pferdeImportiert = 0
|
||||
var pferdeAktualisiert = 0
|
||||
var richterImportiert = 0
|
||||
var richterAktualisiert = 0
|
||||
|
||||
when {
|
||||
fileName.startsWith(FILE_VEREIN) -> {
|
||||
val (n, u) = importiereVereine(lines, fehler)
|
||||
vereineImportiert = n
|
||||
vereineAktualisiert = u
|
||||
}
|
||||
|
||||
fileName.startsWith(FILE_LIZENZ) -> {
|
||||
val (n, u) = importiereReiter(lines, fehler, warnungen)
|
||||
reiterImportiert = n
|
||||
reiterAktualisiert = u
|
||||
}
|
||||
|
||||
fileName.startsWith(FILE_PFERDE) -> {
|
||||
if (mode == ZnsImportMode.FULL) {
|
||||
val (n, u) = importierePferde(lines, fehler)
|
||||
pferdeImportiert = n
|
||||
pferdeAktualisiert = u
|
||||
}
|
||||
}
|
||||
|
||||
fileName.startsWith(FILE_RICHT) -> {
|
||||
if (mode == ZnsImportMode.FULL) {
|
||||
val (n, u) = importiereFunktionaere(lines, fehler, warnungen)
|
||||
richterImportiert = n
|
||||
richterAktualisiert = u
|
||||
}
|
||||
}
|
||||
|
||||
else -> fehler.add("Unbekannte DAT-Datei: $fileName")
|
||||
}
|
||||
|
||||
return ZnsImportResult(
|
||||
vereineImportiert = vereineImportiert,
|
||||
vereineAktualisiert = vereineAktualisiert,
|
||||
reiterImportiert = reiterImportiert,
|
||||
reiterAktualisiert = reiterAktualisiert,
|
||||
pferdeImportiert = pferdeImportiert,
|
||||
pferdeAktualisiert = pferdeAktualisiert,
|
||||
richterImportiert = richterImportiert,
|
||||
richterAktualisiert = richterAktualisiert,
|
||||
fehler = fehler,
|
||||
warnungen = warnungen
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
|
||||
*
|
||||
* @param zipInputStream Der InputStream der ZIP-Datei.
|
||||
* @param mode Der [ZnsImportMode] (Standard: [ZnsImportMode.FULL]).
|
||||
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
|
||||
*/
|
||||
suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult {
|
||||
suspend fun importiereZip(
|
||||
zipInputStream: InputStream,
|
||||
mode: ZnsImportMode = ZnsImportMode.FULL
|
||||
): ZnsImportResult {
|
||||
val dateien = extrahiereDateien(zipInputStream)
|
||||
// println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}")
|
||||
// dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
|
||||
println("[DEBUG_LOG] Gefundene Dateien im ZIP: ${dateien.keys}")
|
||||
dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
|
||||
|
||||
val fehler = mutableListOf<String>()
|
||||
val warnungen = mutableListOf<String>()
|
||||
|
||||
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
|
||||
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
|
||||
val (pferdeNeu, pferdeUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
|
||||
val (richterNeu, richterUpd) = importiereFunktionaere(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
|
||||
|
||||
var pferdeNeu = 0
|
||||
var pferdeUpd = 0
|
||||
var richterNeu = 0
|
||||
var richterUpd = 0
|
||||
|
||||
if (mode == ZnsImportMode.FULL) {
|
||||
val (pNeu, pUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
|
||||
pferdeNeu = pNeu
|
||||
pferdeUpd = pUpd
|
||||
|
||||
val (rNeu, rUpd) = importiereFunktionaere(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
|
||||
richterNeu = rNeu
|
||||
richterUpd = rUpd
|
||||
}
|
||||
|
||||
// Zusätzliche Warnung wenn Dateien fehlen
|
||||
if (dateien[FILE_VEREIN] == null) warnungen.add("Vereinsdaten (VEREIN*.DAT) nicht gefunden.")
|
||||
if (dateien[FILE_LIZENZ] == null) warnungen.add("Reiter/Lizenzdaten (LIZENZ*.DAT) nicht gefunden.")
|
||||
if (mode == ZnsImportMode.FULL) {
|
||||
if (dateien[FILE_PFERDE] == null) warnungen.add("Pferdedaten (PFERDE*.DAT) nicht gefunden.")
|
||||
if (dateien[FILE_RICHT] == null) warnungen.add("Funktionärsdaten (RICHT*.DAT) nicht gefunden.")
|
||||
}
|
||||
|
||||
return ZnsImportResult(
|
||||
vereineImportiert = vereineNeu,
|
||||
@@ -132,7 +254,11 @@ class ZnsImportService(
|
||||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val verein = ZnsVereinParser.parse(zeile) ?: return@forEachIndexed
|
||||
val verein = ZnsVereinParser.parse(zeile)
|
||||
if (verein == null) {
|
||||
if (index < 5) println("[DEBUG_LOG] Parser lieferte null für Zeile ${index + 1}: '$zeile'")
|
||||
return@forEachIndexed
|
||||
}
|
||||
val vorhanden = vereinRepository.findByVereinsNummer(verein.vereinsNummer)
|
||||
if (vorhanden == null) {
|
||||
vereinRepository.save(verein)
|
||||
@@ -167,7 +293,11 @@ class ZnsImportService(
|
||||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val parsed = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed
|
||||
val parsed = ZnsReiterParser.parse(zeile)
|
||||
if (parsed == null) {
|
||||
if (index < 5) println("[DEBUG_LOG] Reiter-Parser lieferte null für Zeile ${index + 1}: '$zeile'")
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
// Relationen auflösen
|
||||
val verein = parsed.vereinsName?.let { vereinRepository.findByExactName(it) }
|
||||
|
||||
@@ -9,9 +9,6 @@ plugins {
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
+20
-1
@@ -2,14 +2,33 @@
|
||||
|
||||
package at.mocode.billing.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
class BillingServiceApplication
|
||||
class BillingServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(BillingServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8087")
|
||||
val appName = env.getProperty("spring.application.name", "billing-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<BillingServiceApplication>(*args)
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
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: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
# health-check-port: 8089
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
port: ${billing.http.port:8089}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:${BILLING_SERVICE_PORT:8087}}
|
||||
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:
|
||||
@@ -26,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)
|
||||
|
||||
+19
-1
@@ -1,9 +1,13 @@
|
||||
package at.mocode.entries.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
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
|
||||
|
||||
@@ -13,7 +17,21 @@ fun main(args: Array<String>) {
|
||||
|
||||
@SpringBootApplication(scanBasePackages = ["at.mocode.entries", "at.mocode.billing", "at.mocode.infrastructure.security"])
|
||||
@EnableAspectJAutoProxy
|
||||
class EntriesServiceApplication {
|
||||
class EntriesServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EntriesServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8084")
|
||||
val appName = env.getProperty("spring.application.name", "entries-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun corsConfigurer(): WebMvcConfigurer {
|
||||
|
||||
@@ -13,14 +13,17 @@ spring:
|
||||
jwk-set-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI:http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs}
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8083
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
flyway:
|
||||
enabled: ${SPRING_FLYWAY_ENABLED:true}
|
||||
|
||||
@@ -10,10 +10,6 @@ plugins {
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,8 +1,12 @@
|
||||
package at.mocode.events.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
/**
|
||||
* Main application class for the Events Service.
|
||||
@@ -11,7 +15,22 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class EventsServiceApplication
|
||||
class EventsServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EventsServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8085")
|
||||
val appName = env.getProperty("spring.application.name", "events-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for the Events Service application.
|
||||
|
||||
@@ -19,9 +19,11 @@ spring:
|
||||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: 8085
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
@@ -35,6 +37,8 @@ management:
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
|
||||
+10
-1
@@ -10,13 +10,22 @@ import kotlin.time.Instant
|
||||
data class Device(
|
||||
val id: UUID = UUID.randomUUID(),
|
||||
val name: String,
|
||||
val expectedName: String? = null, // Falls vom Master vor-registriert
|
||||
val securityKeyHash: String, // Gehasht für Sicherheit
|
||||
val role: DeviceRole = DeviceRole.CLIENT,
|
||||
val lastSyncAt: Instant? = null,
|
||||
val isOnline: Boolean = false,
|
||||
val isSynchronized: Boolean = true,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant = createdAt
|
||||
)
|
||||
|
||||
enum class DeviceRole {
|
||||
MASTER, CLIENT
|
||||
MASTER,
|
||||
CLIENT,
|
||||
RICHTER,
|
||||
ZEITNEHMER,
|
||||
STALLMEISTER,
|
||||
ANZEIGE,
|
||||
PARCOURS_CHEF
|
||||
}
|
||||
|
||||
+3
@@ -13,10 +13,13 @@ object DeviceTable : Table("identity_devices") {
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
val name = varchar("name", 100).uniqueIndex()
|
||||
val expectedName = varchar("expected_name", 100).nullable()
|
||||
val securityKeyHash = varchar("security_key_hash", 255)
|
||||
val role = enumerationByName("role", 20, DeviceRole::class)
|
||||
|
||||
val lastSyncAt = timestamp("last_sync_at").nullable()
|
||||
val isOnline = bool("is_online").default(false)
|
||||
val isSynchronized = bool("is_synchronized").default(true)
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
}
|
||||
|
||||
+9
@@ -33,18 +33,24 @@ class ExposedDeviceRepository : DeviceRepository {
|
||||
if (existing != null) {
|
||||
DeviceTable.update({ DeviceTable.id eq device.id }) {
|
||||
it[name] = device.name
|
||||
it[expectedName] = device.expectedName
|
||||
it[securityKeyHash] = device.securityKeyHash
|
||||
it[role] = device.role
|
||||
it[lastSyncAt] = device.lastSyncAt
|
||||
it[isOnline] = device.isOnline
|
||||
it[isSynchronized] = device.isSynchronized
|
||||
it[updatedAt] = now
|
||||
}
|
||||
} else {
|
||||
DeviceTable.insert {
|
||||
it[id] = device.id
|
||||
it[name] = device.name
|
||||
it[expectedName] = device.expectedName
|
||||
it[securityKeyHash] = device.securityKeyHash
|
||||
it[role] = device.role
|
||||
it[lastSyncAt] = device.lastSyncAt
|
||||
it[isOnline] = device.isOnline
|
||||
it[isSynchronized] = device.isSynchronized
|
||||
it[createdAt] = device.createdAt
|
||||
it[updatedAt] = now
|
||||
}
|
||||
@@ -62,9 +68,12 @@ class ExposedDeviceRepository : DeviceRepository {
|
||||
private fun rowToDevice(row: ResultRow): Device = Device(
|
||||
id = row[DeviceTable.id],
|
||||
name = row[DeviceTable.name],
|
||||
expectedName = row[DeviceTable.expectedName],
|
||||
securityKeyHash = row[DeviceTable.securityKeyHash],
|
||||
role = row[DeviceTable.role],
|
||||
lastSyncAt = row[DeviceTable.lastSyncAt],
|
||||
isOnline = row[DeviceTable.isOnline],
|
||||
isSynchronized = row[DeviceTable.isSynchronized],
|
||||
createdAt = row[DeviceTable.createdAt],
|
||||
updatedAt = row[DeviceTable.updatedAt]
|
||||
)
|
||||
|
||||
+20
-1
@@ -1,10 +1,29 @@
|
||||
package at.mocode.identity.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication(scanBasePackages = ["at.mocode.identity", "at.mocode.infrastructure.security", "at.mocode.backend.infrastructure.persistence"])
|
||||
class IdentityServiceApplication
|
||||
class IdentityServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(IdentityServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8089")
|
||||
val appName = env.getProperty("spring.application.name", "identity-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<IdentityServiceApplication>(*args)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:${IDENTITY_SERVICE_PORT:8088}}
|
||||
port: 8087 # identity-service port
|
||||
|
||||
spring:
|
||||
application:
|
||||
@@ -10,14 +10,17 @@ spring:
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8087
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
|
||||
@@ -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,
|
||||
|
||||
+48
-2
@@ -1,10 +1,56 @@
|
||||
package at.mocode.mail.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.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
|
||||
class MailServiceApplication
|
||||
@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("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<MailServiceApplication>(*args)
|
||||
|
||||
-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,21 +12,50 @@ 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: ${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
|
||||
health-check-port: 8092
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: 8085
|
||||
port: 8092
|
||||
|
||||
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,49 +1,63 @@
|
||||
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}
|
||||
prefer-ip-address: true # Nutze IP im Docker-Netzwerk
|
||||
health-check-path: /actuator/health
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health/readiness
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8086 #8086 # Spring Boot Port (Tomcat), NICHT Ktor (8091)
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
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} # Ktor API Port registrieren
|
||||
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:
|
||||
metrics:
|
||||
export:
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -36,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 ===
|
||||
|
||||
+20
-1
@@ -1,15 +1,34 @@
|
||||
package at.mocode.ping
|
||||
|
||||
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.ComponentScan
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAspectJAutoProxy
|
||||
// Scannt das eigene Service-Package UND das Security-Infrastruktur-Package
|
||||
@ComponentScan(basePackages = ["at.mocode.ping", "at.mocode.infrastructure.security"])
|
||||
class PingServiceApplication
|
||||
class PingServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(PingServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8082")
|
||||
val appName = env.getProperty("spring.application.name", "ping-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<PingServiceApplication>(*args)
|
||||
|
||||
+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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,13 +43,16 @@ spring:
|
||||
consul:
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
enabled: ${SPRING_CLOUD_CONSUL_ENABLED:true}
|
||||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
health-check-path: /actuator/health
|
||||
enabled: ${SPRING_CLOUD_CONSUL_DISCOVERY_ENABLED:true}
|
||||
register: ${SPRING_CLOUD_CONSUL_DISCOVERY_REGISTER:true}
|
||||
prefer-ip-address: ${SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS:true}
|
||||
health-check-path: ${SPRING_CLOUD_CONSUL_DISCOVERY_HEALTH_CHECK_PATH:/actuator/health}
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: ${SERVER_PORT:8082}
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
+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()
|
||||
|
||||
|
||||
+20
-1
@@ -2,14 +2,33 @@ package at.mocode.results.service
|
||||
|
||||
import at.mocode.results.service.application.ResultsService
|
||||
import at.mocode.results.service.domain.Ergebnis
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class ResultsServiceApplication
|
||||
class ResultsServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ResultsServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8088")
|
||||
val appName = env.getProperty("spring.application.name", "results-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<ResultsServiceApplication>(*args)
|
||||
|
||||
@@ -22,11 +22,12 @@ spring:
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: 8088
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:${RESULTS_SERVICE_PORT:8084}}
|
||||
port: 8088
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
+20
-1
@@ -1,12 +1,31 @@
|
||||
package at.mocode.scheduling.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@SpringBootApplication
|
||||
class SchedulingServiceApplication
|
||||
class SchedulingServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(SchedulingServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8089")
|
||||
val appName = env.getProperty("spring.application.name", "scheduling-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SchedulingServiceApplication>(*args)
|
||||
|
||||
@@ -8,15 +8,20 @@ spring:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
instance-id: ${spring.application.name}:${random.value}
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8094
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: 8089
|
||||
port: 8094
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
+20
-1
@@ -3,14 +3,33 @@ package at.mocode.series.service
|
||||
import at.mocode.series.service.application.SeriesService
|
||||
import at.mocode.series.service.domain.Serie
|
||||
import at.mocode.series.service.domain.SeriePunkt
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class SeriesServiceApplication
|
||||
class SeriesServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(SeriesServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8089")
|
||||
val appName = env.getProperty("spring.application.name", "series-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SeriesServiceApplication>(*args)
|
||||
|
||||
@@ -22,11 +22,12 @@ spring:
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: 8093
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: 8090
|
||||
port: 8093
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
||||
@@ -92,8 +92,8 @@ USER ${APP_USER}
|
||||
|
||||
EXPOSE 8095 5005
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS --max-time 2 http://localhost:8095/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:-8095}/actuator/health/readiness || exit 1
|
||||
|
||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||
-XX:+UseG1GC \
|
||||
|
||||
@@ -3,6 +3,7 @@ plugins {
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
|
||||
+19
-1
@@ -2,12 +2,30 @@ package at.mocode.zns.import.service
|
||||
|
||||
import at.mocode.masterdata.domain.repository.*
|
||||
import at.mocode.zns.importer.ZnsImportService
|
||||
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
|
||||
|
||||
@SpringBootApplication
|
||||
class ZnsImportServiceApplication {
|
||||
class ZnsImportServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ZnsImportServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8095")
|
||||
val appName = env.getProperty("spring.application.name", "zns-import-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun znsImportService(
|
||||
|
||||
+8
-2
@@ -3,11 +3,14 @@ package at.mocode.zns.import.service.api
|
||||
import at.mocode.zns.import.service.job.ImportJob
|
||||
import at.mocode.zns.import.service.job.ImportJobRegistry
|
||||
import at.mocode.zns.import.service.job.ZnsImportOrchestrator
|
||||
import at.mocode.zns.importer.ZnsImportMode
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
@Serializable
|
||||
data class ImportStartResponse(val jobId: String)
|
||||
|
||||
@RestController
|
||||
@@ -23,9 +26,12 @@ class ZnsImportController(
|
||||
* Rückgabe: 202 Accepted mit JobId.
|
||||
*/
|
||||
@PostMapping(consumes = ["multipart/form-data"])
|
||||
fun starteImport(@RequestParam("file") file: MultipartFile): ResponseEntity<ImportStartResponse> {
|
||||
fun starteImport(
|
||||
@RequestParam("file") file: MultipartFile,
|
||||
@RequestParam("mode", defaultValue = "FULL") mode: ZnsImportMode
|
||||
): ResponseEntity<ImportStartResponse> {
|
||||
val job = jobRegistry.erstelleJob()
|
||||
orchestrator.starteImport(job.jobId, file.bytes)
|
||||
orchestrator.starteImport(job.jobId, file.bytes, file.originalFilename ?: "zns_import.zip", mode)
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED).body(ImportStartResponse(job.jobId))
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -13,10 +13,8 @@ import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
||||
@Configuration
|
||||
@Profile("dev")
|
||||
class ZnsImportDatabaseConfiguration(
|
||||
@Value("\${spring.datasource.url}") private val jdbcUrl: String,
|
||||
@Value("\${spring.datasource.username}") private val username: String,
|
||||
@@ -26,7 +24,8 @@ class ZnsImportDatabaseConfiguration(
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service...")
|
||||
log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service (JDBC: {})...", jdbcUrl)
|
||||
try {
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||
@@ -44,5 +43,8 @@ class ZnsImportDatabaseConfiguration(
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Fehler bei der Datenbank-Initialisierung: {}", e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -1,12 +1,15 @@
|
||||
package at.mocode.zns.import.service.job
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Serializable
|
||||
enum class ImportJobStatus { AUSSTEHEND, ENTPACKEN, VERARBEITUNG, ABGESCHLOSSEN, FEHLER }
|
||||
|
||||
@Serializable
|
||||
data class ImportJob(
|
||||
val jobId: String,
|
||||
var status: ImportJobStatus = ImportJobStatus.AUSSTEHEND,
|
||||
|
||||
+21
-8
@@ -1,6 +1,6 @@
|
||||
package at.mocode.zns.import.service.job
|
||||
|
||||
import at.mocode.zns.importer.ZnsImportResult
|
||||
import at.mocode.zns.importer.ZnsImportMode
|
||||
import at.mocode.zns.importer.ZnsImportService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -19,16 +19,22 @@ class ZnsImportOrchestrator(
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
fun starteImport(jobId: String, zipBytes: ByteArray) {
|
||||
fun starteImport(jobId: String, bytes: ByteArray, fileName: String, mode: ZnsImportMode = ZnsImportMode.FULL) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5)
|
||||
println("[DEBUG_LOG] Starte Import Job $jobId (File: $fileName, Size: ${bytes.size} bytes)")
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Bereite Datei vor...", 5)
|
||||
|
||||
// Archivierung
|
||||
archiviereZip(zipBytes)
|
||||
archiviereDatei(bytes, fileName)
|
||||
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.VERARBEITUNG, "Verarbeite ZNS-Daten...", 20)
|
||||
val result = service.importiereZip(zipBytes.inputStream())
|
||||
val result = service.importiereStream(bytes.inputStream(), fileName, mode)
|
||||
|
||||
println("[DEBUG_LOG] Import Ergebnis: ${result.zusammenfassung()}")
|
||||
if (result.fehler.isNotEmpty()) {
|
||||
println("[DEBUG_LOG] Fehler im Import: ${result.fehler.joinToString()}")
|
||||
}
|
||||
|
||||
jobRegistry.aktualisiereStatus(
|
||||
jobId, ImportJobStatus.ABGESCHLOSSEN,
|
||||
@@ -40,20 +46,27 @@ class ZnsImportOrchestrator(
|
||||
job.warnungen.addAll(result.warnungen)
|
||||
}
|
||||
}.onFailure { ex ->
|
||||
println("[DEBUG_LOG] Kritischer Fehler im ZnsImportOrchestrator: ${ex.message}")
|
||||
ex.printStackTrace()
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.FEHLER, "Fehler: ${ex.message}")
|
||||
jobRegistry.findeJob(jobId)?.fehler?.add(ex.message ?: "Unbekannter Fehler")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun archiviereZip(bytes: ByteArray) {
|
||||
private fun archiviereDatei(bytes: ByteArray, originalFileName: String) {
|
||||
try {
|
||||
val dir = File(archivePath)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
if (!dir.exists()) {
|
||||
val success = dir.mkdirs()
|
||||
println("[DEBUG_LOG] Archiv-Verzeichnis erstellt ($archivePath): $success")
|
||||
}
|
||||
|
||||
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
|
||||
val archiveFile = File(dir, "zns_import_$timestamp.zip")
|
||||
val extension = originalFileName.substringAfterLast(".", "bin")
|
||||
val archiveFile = File(dir, "zns_import_${timestamp}.$extension")
|
||||
archiveFile.writeBytes(bytes)
|
||||
println("[DEBUG_LOG] Datei archiviert: ${archiveFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
// Archivierung schlägt fehl -> Loggen aber Import nicht abbrechen
|
||||
println("[WARN] Archivierung der ZNS-Datei fehlgeschlagen: ${e.message}")
|
||||
|
||||
@@ -28,9 +28,12 @@ spring:
|
||||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-interval: 15s
|
||||
health-check-port: 8095
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
@@ -39,6 +42,8 @@ management:
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
|
||||
app:
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
+25
-2
@@ -38,6 +38,8 @@ plugins {
|
||||
// ### ALLPROJECTS CONFIGURATION ###
|
||||
// ##################################################################
|
||||
|
||||
val isWasmEnabled = findProperty("enableWasm")?.toString()?.toBoolean() ?: false
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Zentrale Versionierung — liest version.properties (SemVer)
|
||||
// ---------------------------------------------------------------
|
||||
@@ -92,7 +94,7 @@ subprojects {
|
||||
minHeapSize = "512m"
|
||||
maxHeapSize = "2g"
|
||||
// Parallel test execution for better performance
|
||||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 4).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -111,7 +113,28 @@ subprojects {
|
||||
// (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings)
|
||||
// to avoid compiler-flag incompatibilities across toolchains.
|
||||
|
||||
// (B) JS test executable compilation/sync is currently very noisy (duplicate resource copying from jsMain + jsTest).
|
||||
// (B) Conditional Wasm/JS Target handling based on `enableWasm` property
|
||||
// This significantly reduces build times during Desktop development.
|
||||
// Flag is defined at the beginning of the script.
|
||||
|
||||
// If Wasm is disabled, we skip the intensive JS/WASM compilation tasks
|
||||
// for modules that are not strictly JVM-only.
|
||||
if (!isWasmEnabled) {
|
||||
tasks.matching {
|
||||
val n = it.name
|
||||
n.contains("wasmJs", ignoreCase = true) ||
|
||||
n.contains("KotlinJs", ignoreCase = true) ||
|
||||
n.contains("JsIr", ignoreCase = true) ||
|
||||
n.contains("packageJsUpper", ignoreCase = true) ||
|
||||
n.contains("jsProcessResources", ignoreCase = true) ||
|
||||
n.contains("wasmJsProcessResources", ignoreCase = true)
|
||||
}.configureEach {
|
||||
enabled = false
|
||||
group = "disabled"
|
||||
}
|
||||
}
|
||||
|
||||
// (C) JS test executable compilation/sync is currently very noisy (duplicate resource copying from jsMain + jsTest).
|
||||
// We disable JS/WASM JS test executables in CI/build to keep output warning-free.
|
||||
tasks.matching {
|
||||
val n = it.name
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user