Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1690da3fab | |||
| cb6e0103e7 | |||
| 98d0bf0c7b | |||
| 0a90b57c2a | |||
| 0ab62a2752 | |||
| 6070709bf2 | |||
| 763c2a9157 | |||
| 4f715d10bb | |||
| 0b830eb675 | |||
| 4c37ecb952 | |||
| c25ef17a4a | |||
| e5e3b4cfec | |||
| 7d064853e5 | |||
| 387180c12c | |||
| 49393d3eac | |||
| e389fe9bce | |||
| 1a4753cd73 | |||
| ece3f8bf78 | |||
| 8d176ce955 | |||
| 280db663c7 | |||
| 74ef6424b7 | |||
| 3959168695 | |||
| 04a435df1d | |||
| 3aaf5cc59c | |||
| a2d94bbc7e | |||
| 95a130c72e | |||
| 223bf77776 |
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
REPO_ROOT="$(resolve_repo_root)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# check-docs-drift.sh
|
||||
# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur.
|
||||
# - Kein Guidelines-System mehr.
|
||||
# - Single Source of Truth: `docs/`
|
||||
|
||||
err=0
|
||||
|
||||
has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; }
|
||||
miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; }
|
||||
|
||||
# Harte Altlast-Pfade dürfen nicht mehr vorkommen
|
||||
if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
|
||||
# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein)
|
||||
has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway"
|
||||
has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway"
|
||||
miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor"
|
||||
has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway"
|
||||
miss docs/01_Architecture/c4/02-container-de.puml "Ktor"
|
||||
|
||||
exit $err
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Common helpers for AI guardrail scripts
|
||||
|
||||
# Robustly resolve the repository root directory.
|
||||
# Strategy: prefer Git; fallback to marker search upwards; last resort: current dir.
|
||||
resolve_repo_root() {
|
||||
local start
|
||||
start="${1:-$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)}"
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
if git -C "$start" rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
git -C "$start" rev-parse --show-toplevel
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
local dir
|
||||
dir="$(cd "$start" && pwd)"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -f "$dir/gradlew" ] || [ -f "$dir/settings.gradle.kts" ] || [ -d "$dir/.git" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
pwd
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
REPO_ROOT="$(resolve_repo_root)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
mkdir -p build/diagrams
|
||||
shopt -s nullglob
|
||||
for f in docs/architecture/c4/*.puml; do
|
||||
docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams"
|
||||
echo "Rendered build/diagrams/$(basename "${f%.puml}").svg"
|
||||
done
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
REPO_ROOT="$(resolve_repo_root)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
QUICK_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--quick)
|
||||
QUICK_MODE=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
cat << 'EOF'
|
||||
Docs Link-Validierung
|
||||
|
||||
USAGE:
|
||||
./.ai/scripts/validate-links.sh [--quick]
|
||||
|
||||
BESCHREIBUNG:
|
||||
Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade.
|
||||
Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...).
|
||||
|
||||
OPTIONEN:
|
||||
--quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert).
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unbekannter Parameter: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
root = Path.cwd()
|
||||
docs_dir = root / "docs"
|
||||
|
||||
if not docs_dir.is_dir():
|
||||
print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Veraltete Pfad-Prüfungen wurden entfernt; Fokus auf Link-Integrität.
|
||||
FORBIDDEN_SUBSTRINGS = []
|
||||
|
||||
md_files = sorted(docs_dir.rglob("*.md"))
|
||||
|
||||
link_pattern = re.compile(r"\]\(([^)]+)\)")
|
||||
|
||||
errors = 0
|
||||
|
||||
def is_external(target: str) -> bool:
|
||||
t = target.lower()
|
||||
return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:")
|
||||
|
||||
def strip_fragment_and_query(target: str) -> str:
|
||||
target = target.split("#", 1)[0]
|
||||
target = target.split("?", 1)[0]
|
||||
return target
|
||||
|
||||
for f in md_files:
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
for forbidden in FORBIDDEN_SUBSTRINGS:
|
||||
if forbidden in text:
|
||||
print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}")
|
||||
errors += 1
|
||||
|
||||
for match in link_pattern.finditer(text):
|
||||
target = match.group(1).strip()
|
||||
|
||||
if not target:
|
||||
continue
|
||||
if is_external(target):
|
||||
continue
|
||||
if target.startswith("#"):
|
||||
continue
|
||||
|
||||
if target.startswith("<") and target.endswith(">"):
|
||||
target = target[1:-1]
|
||||
|
||||
target = unquote(strip_fragment_and_query(target))
|
||||
|
||||
if target.startswith("/"):
|
||||
continue
|
||||
|
||||
if ":" in target.split("/", 1)[0]:
|
||||
# z.B. "vscode:..."
|
||||
continue
|
||||
|
||||
resolved = (f.parent / target).resolve()
|
||||
|
||||
try:
|
||||
resolved.relative_to(root.resolve())
|
||||
except ValueError:
|
||||
print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
if resolved.is_dir():
|
||||
if not (resolved / "README.md").is_file():
|
||||
print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
if not resolved.exists():
|
||||
print(f"[ERROR] Broken link: {f} -> {target}")
|
||||
errors += 1
|
||||
|
||||
if errors:
|
||||
print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft")
|
||||
PY
|
||||
+1
-1
@@ -193,7 +193,7 @@ secrets/
|
||||
# ===================================================================
|
||||
TODO*.md
|
||||
NOTES*.md
|
||||
**/.junie/
|
||||
.junie/
|
||||
|
||||
# ===================================================================
|
||||
# Keep essential files (override exclusions)
|
||||
|
||||
@@ -18,7 +18,7 @@ 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]') }}
|
||||
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
name: Compose Desktop — Tests (headless) & Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Build and Publish Docker Images
|
||||
run-name: Build & Publish by @${{ github.actor }}
|
||||
run-name: Build & Publish by @${{ gitea.actor }}
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -117,8 +117,8 @@ jobs:
|
||||
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,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=raw,value=latest,enable=${{ gitea.ref == 'refs/heads/main' }}
|
||||
type=sha,format=long,enable=${{ gitea.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -132,5 +132,5 @@ jobs:
|
||||
provenance: false
|
||||
sbom: false
|
||||
build-args: |
|
||||
BUILD_DATE=${{ github.event.head_commit.timestamp || 'unknown' }}
|
||||
VERSION=${{ github.sha }}
|
||||
BUILD_DATE=${{ gitea.event.head_commit.timestamp || 'unknown' }}
|
||||
VERSION=${{ gitea.sha }}
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
chmod +x install-conveyor.sh
|
||||
./install-conveyor.sh
|
||||
fi
|
||||
echo "$HOME/.conveyor/bin" >> $GITHUB_PATH
|
||||
echo "$HOME/.conveyor/bin" >> $GITEA_PATH
|
||||
|
||||
- name: Windows .msi mit Conveyor bauen
|
||||
run: |
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
jobs:
|
||||
no-hardcoded-versions:
|
||||
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
|
||||
if: ${{ !contains(github.event.head_commit.message, '[planb]') }}
|
||||
if: ${{ !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -23,7 +23,7 @@ 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]') }}
|
||||
if: ${{ !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.read-version.outputs.version }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Git-Tag erstellen & pushen
|
||||
if: steps.check-tag.outputs.already_tagged == 'false' && github.event.inputs.dry_run != 'true'
|
||||
if: steps.check-tag.outputs.already_tagged == 'false' && gitea.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
TAG="${{ steps.read-version.outputs.tag }}"
|
||||
VERSION="${{ steps.read-version.outputs.version }}"
|
||||
@@ -80,7 +80,7 @@ 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]') }}
|
||||
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: tag-release
|
||||
|
||||
@@ -88,11 +88,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup JDK 21 (Temurin)
|
||||
- name: Setup JDK 25 (Temurin)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
- name: Gradle cache
|
||||
uses: actions/cache@v4
|
||||
@@ -128,7 +128,7 @@ 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]') }}
|
||||
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: windows-latest
|
||||
needs: tag-release
|
||||
|
||||
@@ -136,11 +136,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup JDK 21 (Temurin)
|
||||
- name: Setup JDK 25 (Temurin)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
- name: Gradle cache
|
||||
uses: actions/cache@v4
|
||||
@@ -179,11 +179,11 @@ jobs:
|
||||
steps:
|
||||
- name: Summary ausgeben
|
||||
run: |
|
||||
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Artefakt | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITEA_STEP_SUMMARY
|
||||
echo "" >> $GITEA_STEP_SUMMARY
|
||||
echo "| Artefakt | Status |" >> $GITEA_STEP_SUMMARY
|
||||
echo "|----------|--------|" >> $GITEA_STEP_SUMMARY
|
||||
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITEA_STEP_SUMMARY
|
||||
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITEA_STEP_SUMMARY
|
||||
echo "" >> $GITEA_STEP_SUMMARY
|
||||
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITEA_STEP_SUMMARY
|
||||
|
||||
@@ -56,3 +56,7 @@ desktop.ini
|
||||
docs/temp/
|
||||
docs/Bin/
|
||||
docs/_archive/
|
||||
|
||||
# Conveyor
|
||||
conveyor.rootkey
|
||||
output/
|
||||
|
||||
@@ -1,43 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# check-docs-drift.sh
|
||||
# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur.
|
||||
# - Kein Guidelines-System mehr.
|
||||
# - Single Source of Truth: `docs/`
|
||||
|
||||
err=0
|
||||
|
||||
has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; }
|
||||
miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; }
|
||||
|
||||
# Harte Altlast-Pfade dürfen nicht mehr vorkommen
|
||||
if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
|
||||
# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein)
|
||||
has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway"
|
||||
has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway"
|
||||
miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor"
|
||||
has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway"
|
||||
miss docs/01_Architecture/c4/02-container-de.puml "Ktor"
|
||||
|
||||
exit $err
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p build/diagrams
|
||||
shopt -s nullglob
|
||||
for f in docs/architecture/c4/*.puml; do
|
||||
docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams"
|
||||
echo "Rendered build/diagrams/$(basename "${f%.puml}").svg"
|
||||
done
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/render-plantuml.sh" "$@"
|
||||
|
||||
@@ -1,136 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# validate-links.sh - Link-Validierung für Projektdokumentation (`docs/**`).
|
||||
# Zweck: Guardrail für die "Docs-as-Code"-Strategie.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
QUICK_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--quick)
|
||||
QUICK_MODE=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
cat << 'EOF'
|
||||
Docs Link-Validierung
|
||||
|
||||
USAGE:
|
||||
./.junie/scripts/validate-links.sh [--quick]
|
||||
|
||||
BESCHREIBUNG:
|
||||
Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade.
|
||||
Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...).
|
||||
|
||||
OPTIONEN:
|
||||
--quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert).
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unbekannter Parameter: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
root = Path.cwd()
|
||||
docs_dir = root / "docs"
|
||||
|
||||
if not docs_dir.is_dir():
|
||||
print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Veraltete Pfad-Prüfungen wurden entfernt, da sie zu wartungsintensiv waren.
|
||||
# Das Skript konzentriert sich nun auf die Validierung der Link-Integrität.
|
||||
FORBIDDEN_SUBSTRINGS = []
|
||||
|
||||
md_files = sorted(docs_dir.rglob("*.md"))
|
||||
|
||||
link_pattern = re.compile(r"\]\(([^)]+)\)")
|
||||
|
||||
errors = 0
|
||||
|
||||
def is_external(target: str) -> bool:
|
||||
t = target.lower()
|
||||
return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:")
|
||||
|
||||
def strip_fragment_and_query(target: str) -> str:
|
||||
# remove fragment and query parts
|
||||
target = target.split("#", 1)[0]
|
||||
target = target.split("?", 1)[0]
|
||||
return target
|
||||
|
||||
for f in md_files:
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
for forbidden in FORBIDDEN_SUBSTRINGS:
|
||||
if forbidden in text:
|
||||
print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}")
|
||||
errors += 1
|
||||
|
||||
for match in link_pattern.finditer(text):
|
||||
target = match.group(1).strip()
|
||||
|
||||
if not target:
|
||||
continue
|
||||
if is_external(target):
|
||||
continue
|
||||
if target.startswith("#"):
|
||||
continue
|
||||
|
||||
# drop angle brackets <...> used in markdown for urls with spaces
|
||||
if target.startswith("<") and target.endswith(">"):
|
||||
target = target[1:-1]
|
||||
|
||||
target = unquote(strip_fragment_and_query(target))
|
||||
|
||||
# ignore absolute paths in the repo (we treat them as doc-style links; validate only if relative)
|
||||
if target.startswith("/"):
|
||||
continue
|
||||
|
||||
# ignore non-file targets (e.g. empty or protocol-less anchors)
|
||||
if ":" in target.split("/", 1)[0]:
|
||||
# things like "vscode:..." etc.
|
||||
continue
|
||||
|
||||
# treat as file path relative to markdown file
|
||||
resolved = (f.parent / target).resolve()
|
||||
|
||||
# keep validation within repo
|
||||
try:
|
||||
resolved.relative_to(root.resolve())
|
||||
except ValueError:
|
||||
print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
# allow directories if they contain README.md
|
||||
if resolved.is_dir():
|
||||
if not (resolved / "README.md").is_file():
|
||||
print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
if not resolved.exists():
|
||||
print(f"[ERROR] Broken link: {f} -> {target}")
|
||||
errors += 1
|
||||
|
||||
if errors:
|
||||
print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft")
|
||||
PY
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# .aiignore - Verhindert Token-Waste für Nolik
|
||||
|
||||
# Abhängigkeiten & Binaries
|
||||
build/
|
||||
.gradle/
|
||||
*.jar
|
||||
*.deb
|
||||
*.msi
|
||||
|
||||
# Sensible Daten (auch lokal!)
|
||||
.env
|
||||
.env.*
|
||||
config/docker/certs/
|
||||
*.pem
|
||||
*.jks
|
||||
postgres-data/
|
||||
valkey-data/
|
||||
|
||||
# Doku-Builds (Nolik soll die Source-Files in docs/ lesen, nicht die HTML-Exporte)
|
||||
build/dokka/
|
||||
docs/Neumarkt2026/*.pdf
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
|
||||
@@ -20,7 +20,7 @@ Die gesamte Projektdokumentation (Architektur, Fachdomäne, Entwickler-Anleitung
|
||||
| [03_Domain](./docs/03_Domain) | Fachlichkeit, Turnierregeln, Entities |
|
||||
| [07_Infrastructure](./docs/07_Infrastructure) | Docker, Keycloak, CI/CD, Zora-Infrastruktur |
|
||||
|
||||
Wesentliche Architektur-Referenz: [Offline‑First Desktop & Backend (Kurzkonzept)](./docs/01_Architecture/konzept-offline-first-desktop-backend-de.md)
|
||||
Wesentliche Architektur-Referenz: [Offline‑First Desktop & Backend (Kurzkonzept)](./docs/01_Architecture/Concepts/konzept-offline-first-desktop-backend-de.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -113,3 +113,9 @@ Beiträge sind willkommen. Bitte lies zunächst die Entwickler-Guides unter [`do
|
||||
## 📜 Lizenz
|
||||
|
||||
Dieses Projekt steht unter der [MIT License](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## Test
|
||||
|
||||
Das ist der 2. Versuch über Remote zu Committen
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import io.valkey.springframework.data.valkey.core.ValkeyTemplate
|
||||
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.testcontainers.containers.GenericContainer
|
||||
@@ -70,7 +70,7 @@ class ValkeyDistributedCachePerformanceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test cache performance with high concurrent access`() = runTest {
|
||||
fun `test cache performance with high concurrent access`() = runBlocking {
|
||||
logger.info { "Starting concurrent access test" }
|
||||
val numberOfCoroutines = 100
|
||||
val operationsPerCoroutine = 50
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
|
||||
package at.mocode.zns.importer
|
||||
|
||||
import at.mocode.core.domain.model.ReiterLizenz
|
||||
import at.mocode.masterdata.domain.repository.*
|
||||
import at.mocode.zns.parser.ZnsFunktionaerParser
|
||||
import at.mocode.zns.parser.ZnsPferdParser
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
+1
-10
@@ -3,6 +3,7 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.DatenQuelleE
|
||||
import at.mocode.core.domain.model.ReiterLizenz
|
||||
import at.mocode.core.domain.model.ReiterLizenzKlasseE
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.LocalDateSerializer
|
||||
@@ -14,16 +15,6 @@ import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Serializable
|
||||
data class ReiterLizenz(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val lizenzId: Uuid = Uuid.random(),
|
||||
val lizenzTyp: String, // STARTKARTE, REITERLIZENZ, FAHRLIZENZ
|
||||
val kuerzel: String,
|
||||
@Serializable(with = LocalDateSerializer::class)
|
||||
val gueltigBis: LocalDate? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Domain model representing a rider (Reiter) in the actor-context.
|
||||
*
|
||||
|
||||
+1
-1
@@ -4,10 +4,10 @@ package at.mocode.masterdata.infrastructure.persistence.reiter
|
||||
|
||||
import at.mocode.core.domain.model.DatenQuelleE
|
||||
import at.mocode.core.domain.model.ReiterAltersKlasseE
|
||||
import at.mocode.core.domain.model.ReiterLizenz
|
||||
import at.mocode.core.domain.model.ReiterLizenzKlasseE
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.masterdata.domain.model.Reiter
|
||||
import at.mocode.masterdata.domain.model.ReiterLizenz
|
||||
import at.mocode.masterdata.domain.repository.ReiterRepository
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION=9.4.1
|
||||
ARG JAVA_VERSION=25
|
||||
ARG GRADLE_VERSION=9.5.0
|
||||
ARG JAVA_VERSION=25.0.2
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION=1.0.0-SNAPSHOT
|
||||
|
||||
|
||||
+45
-24
@@ -38,7 +38,7 @@ plugins {
|
||||
// ### ALLPROJECTS CONFIGURATION ###
|
||||
// ##################################################################
|
||||
|
||||
val isWasmEnabled = findProperty("enableWasm")?.toString()?.toBoolean() ?: false
|
||||
val isWasmEnabled: Boolean = findProperty("enableWasm")?.toString()?.toBoolean() ?: false
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Zentrale Versionierung — liest version.properties (SemVer)
|
||||
@@ -47,10 +47,10 @@ val versionProps =
|
||||
java.util.Properties().also { props ->
|
||||
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
||||
}
|
||||
val vMajor = versionProps.getProperty("VERSION_MAJOR", "1")
|
||||
val vMinor = versionProps.getProperty("VERSION_MINOR", "0")
|
||||
val vPatch = versionProps.getProperty("VERSION_PATCH", "0")
|
||||
val vQualifier = versionProps.getProperty("VERSION_QUALIFIER", "").trim()
|
||||
val vMajor: String = versionProps.getProperty("VERSION_MAJOR", "1")
|
||||
val vMinor: String = versionProps.getProperty("VERSION_MINOR", "0")
|
||||
val vPatch: String = versionProps.getProperty("VERSION_PATCH", "0")
|
||||
val vQualifier: String = versionProps.getProperty("VERSION_QUALIFIER", "").trim()
|
||||
val semVer = if (vQualifier.isBlank()) "$vMajor.$vMinor.$vPatch" else "$vMajor.$vMinor.$vPatch-$vQualifier"
|
||||
|
||||
allprojects {
|
||||
@@ -90,7 +90,7 @@ subprojects {
|
||||
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
||||
// Suppress ByteBuddy/Mockito dynamic agent loading warnings (Java 21+)
|
||||
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
||||
// Increase test JVM memory with a stable configuration
|
||||
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
minHeapSize = "512m"
|
||||
maxHeapSize = "2g"
|
||||
// Parallel test execution for better performance
|
||||
@@ -113,7 +113,7 @@ subprojects {
|
||||
// (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings)
|
||||
// to avoid compiler-flag incompatibilities across toolchains.
|
||||
|
||||
// (B) Conditional Wasm/JS Target handling based on `enableWasm` property
|
||||
// (B) Conditional Wasm/JS Target handling based on the ` enableWasm ` property
|
||||
// This significantly reduces build times during Desktop development.
|
||||
// Flag is defined at the beginning of the script.
|
||||
|
||||
@@ -166,6 +166,7 @@ subprojects {
|
||||
jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false")
|
||||
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
||||
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
||||
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
maxHeapSize = "2g"
|
||||
dependsOn("testClasses")
|
||||
}
|
||||
@@ -175,20 +176,30 @@ subprojects {
|
||||
// Applies to all Exec-based tasks (covers Yarn/NPM invocations used by Kotlin JS plugin)
|
||||
tasks.withType<Exec>().configureEach {
|
||||
// Merge existing NODE_OPTIONS with --no-deprecation
|
||||
val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||||
val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||||
val current: String? = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||||
val merged: String = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||||
environment("NODE_OPTIONS", merged)
|
||||
// Also set the legacy switch to silence warnings entirely
|
||||
environment("NODE_NO_WARNINGS", "1")
|
||||
// Set a Chrome binary path to avoid snap permission issues
|
||||
if (System.getProperty("os.name").contains("Linux", ignoreCase = true)) {
|
||||
environment("CHROME_BIN", "/usr/bin/google-chrome-stable")
|
||||
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
||||
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Detekt & Ktlint default setup
|
||||
// ------------------------------
|
||||
// PERFORMANCE: Deaktiviert standardmäßig in jedem Build, nur explizit ausführen
|
||||
tasks.withType<Detekt>().configureEach {
|
||||
enabled = project.hasProperty("runStaticAnalysis")
|
||||
}
|
||||
tasks.matching { it.name == "ktlintCheck" }.configureEach {
|
||||
enabled = project.hasProperty("runStaticAnalysis")
|
||||
}
|
||||
|
||||
plugins.withId("io.gitlab.arturbosch.detekt") {
|
||||
extensions.configure(DetektExtension::class.java) {
|
||||
buildUponDefaultConfig = true
|
||||
@@ -267,7 +278,6 @@ tasks.register("checkBundleBudget") {
|
||||
}
|
||||
|
||||
shells.forEach { shell ->
|
||||
val key = shell.path.trimStart(':').replace(':', '/') // or use a colon form for budgets keys below
|
||||
val colonKey = shell.path.trimStart(':').replace('/', ':').trim() // ensure ":a:b:c"
|
||||
// Budgets are keyed by a Gradle path with colons but without leading colon in config for readability
|
||||
val budgetKeyCandidates =
|
||||
@@ -362,8 +372,8 @@ tasks.register("staticAnalysis") {
|
||||
|
||||
// Apply Dokka (V2) automatically to Kotlin subprojects
|
||||
subprojects {
|
||||
plugins.withId("org.jetbrains.kotlin.jvm") { apply(plugin = "org.jetbrains.dokka") }
|
||||
plugins.withId("org.jetbrains.kotlin.multiplatform") { apply(plugin = "org.jetbrains.dokka") }
|
||||
plugins.withId("org.jetbrains.kotlin.jvm") { pluginManager.apply("org.jetbrains.dokka") }
|
||||
plugins.withId("org.jetbrains.kotlin.multiplatform") { pluginManager.apply("org.jetbrains.dokka") }
|
||||
}
|
||||
|
||||
// Aggregate tasks to build multi-module docs in Markdown (GFM) and HTML
|
||||
@@ -372,27 +382,36 @@ val dokkaAll =
|
||||
tasks.register("dokkaAll") {
|
||||
group = "documentation"
|
||||
description = "Builds Dokka (V2) for all modules and aggregates outputs under build/dokka/all"
|
||||
// Trigger Dokka generation in all subprojects that have the Dokka plugin
|
||||
dependsOn(
|
||||
// PERFORMANCE: Nur ausführen wenn explizit gefordert
|
||||
enabled = project.hasProperty("runDokka")
|
||||
|
||||
// Capture required values for configuration cache
|
||||
val rootBuildDir = layout.buildDirectory.get().asFile
|
||||
val subprojectData =
|
||||
subprojects
|
||||
.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }
|
||||
.map { "${it.path}:dokkaGenerate" },
|
||||
)
|
||||
.map { p ->
|
||||
Triple(p.path, p.name, p.layout.buildDirectory.get().asFile)
|
||||
}
|
||||
|
||||
// Trigger Dokka generation in all subprojects that have the Dokka plugin
|
||||
dependsOn(subprojectData.map { "${it.first}:dokkaGenerate" })
|
||||
|
||||
doLast {
|
||||
val dest = layout.buildDirectory.dir("dokka/all").get().asFile
|
||||
val dest = File(rootBuildDir, "dokka/all")
|
||||
if (dest.exists()) dest.deleteRecursively()
|
||||
dest.mkdirs()
|
||||
|
||||
val modules = mutableListOf<Pair<String, String>>()
|
||||
|
||||
subprojects.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }.forEach { p ->
|
||||
subprojectData.forEach { (pPath, pName, pBuildDir) ->
|
||||
// Dokka V2 writes into build/dokka/html
|
||||
val outHtml = p.layout.buildDirectory.dir("dokka/html").get().asFile
|
||||
val outHtml = File(pBuildDir, "dokka/html")
|
||||
if (outHtml.exists()) {
|
||||
val modulePath = p.path.trimStart(':').replace(':', '/')
|
||||
val modulePath = pPath.trimStart(':').replace(':', '/')
|
||||
val targetDir = File(dest, modulePath)
|
||||
outHtml.copyRecursively(targetDir, overwrite = true)
|
||||
modules.add(p.name to modulePath)
|
||||
modules.add(pName to modulePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,17 +470,19 @@ tasks.register("docs") {
|
||||
// Apply Node warning suppression on root project Exec tasks as well
|
||||
// Ensures aggregated Kotlin/JS tasks created at root (e.g., kotlinNpmInstall) inherit the env
|
||||
tasks.withType<Exec>().configureEach {
|
||||
val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||||
val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||||
val current: String? = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||||
val merged: String = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||||
environment("NODE_OPTIONS", merged)
|
||||
environment("NODE_NO_WARNINGS", "1")
|
||||
// Set a Chrome binary path to avoid snap permission issues
|
||||
if (System.getProperty("os.name").contains("Linux", ignoreCase = true)) {
|
||||
environment("CHROME_BIN", "/usr/bin/google-chrome-stable")
|
||||
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
||||
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.wrapper {
|
||||
gradleVersion = "9.4.1"
|
||||
gradleVersion = "9.5.0"
|
||||
distributionType = Wrapper.DistributionType.BIN
|
||||
}
|
||||
|
||||
+19
-35
@@ -1,62 +1,46 @@
|
||||
# =============================================================================
|
||||
# Conveyor Configuration for Meldestelle Desktop App
|
||||
# =============================================================================
|
||||
# Dieser Build-Weg ermöglicht das Cross-Packaging für Windows (MSI) auf Linux.
|
||||
# Dokumentation: https://conveyor.hydraulic.dev/
|
||||
# =============================================================================
|
||||
|
||||
include required("/stdlib/jdk/21/amazon.conf")
|
||||
include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
|
||||
|
||||
# Basis-Import der Gradle-Konfiguration (sofern das Plugin genutzt wird,
|
||||
# aber wir definieren es hier explizit für maximale Kontrolle im CI/CD).
|
||||
app {
|
||||
# Anzeige-Name und Vendor
|
||||
display-name = "Meldestelle"
|
||||
rdns-name = "at.mocode.meldestelle"
|
||||
vendor = "mo-code.at"
|
||||
contact-email = "support@mo-code.at"
|
||||
version = "1.0.1"
|
||||
description = "ÖTO-konforme Turnier-Meldestelle – Profi Desktop App"
|
||||
|
||||
# Version aus version.properties (Conveyor kann HOCON-Variablen nutzen)
|
||||
# Für diesen Task hart codiert oder via CLI-Flag --variable übergeben.
|
||||
version = "1.0.0"
|
||||
# Ziel-Plattformen: Windows und Linux
|
||||
machines = [ windows.amd64, linux.amd64.glibc ]
|
||||
|
||||
# Beschreibung
|
||||
description = "ÖTO-konforme Turnier-Meldestelle – Desktop App"
|
||||
site.base-url = "localhost"
|
||||
|
||||
# Ziel-Plattformen
|
||||
# Wir konzentrieren uns auf Windows, können aber Linux/Mac später ergänzen.
|
||||
site.base-url = "localhost" # Später echte Update-URL
|
||||
|
||||
# Icons
|
||||
# Icons werden im Ordner gesucht
|
||||
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.png"
|
||||
|
||||
# Einbetten der JRE (Temurin 21 wie in CI genutzt)
|
||||
jvm {
|
||||
gui {
|
||||
main-class = "at.mocode.frontend.shell.desktop.MainKt"
|
||||
}
|
||||
|
||||
# JVM-Argumente (analog build.gradle.kts)
|
||||
jvm-options = [
|
||||
"-Xms128m",
|
||||
"-Xmx512m",
|
||||
"-Dfile.encoding=UTF-8"
|
||||
"-Xms256m",
|
||||
"-Xmx1024m",
|
||||
"-Dfile.encoding=UTF-8",
|
||||
"--enable-native-access=ALL-UNNAMED"
|
||||
]
|
||||
}
|
||||
|
||||
# Input-Dateien: Hier ziehen wir die Uber-JAR oder die Gradle-Outputs.
|
||||
# Da wir plattformunabhängig bleiben wollen, nutzen wir das Gradle-Output-Dir.
|
||||
inputs += "frontend/shells/meldestelle-desktop/build/libs/meldestelle-desktop-jvm-*.jar"
|
||||
# JARs aus dem Gradle-Build
|
||||
inputs += "frontend/shells/meldestelle-desktop/build/libs/*.jar"
|
||||
|
||||
# Windows-spezifische Einstellungen
|
||||
windows {
|
||||
# Icon als .ico
|
||||
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.ico"
|
||||
# GUID für Upgrades (muss stabil bleiben)
|
||||
upgrade-uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
# Menü-Eintrag
|
||||
menu-group = "Meldestelle"
|
||||
# Verknüpfung
|
||||
desktop-shortcut = true
|
||||
}
|
||||
|
||||
linux {
|
||||
debian.control.depends = "libasound2, libgl1-mesa-glx, libx11-6"
|
||||
}
|
||||
}
|
||||
|
||||
conveyor.compatibility-level = 22
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.core.domain.model
|
||||
|
||||
import at.mocode.core.domain.serialization.LocalDateSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Serializable
|
||||
data class ReiterLizenz(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val lizenzId: Uuid = Uuid.random(),
|
||||
val lizenzTyp: String, // STARTKARTE, REITERLIZENZ, FAHRLIZENZ
|
||||
val kuerzel: String,
|
||||
@Serializable(with = LocalDateSerializer::class)
|
||||
val gueltigBis: LocalDate? = null
|
||||
)
|
||||
@@ -1,10 +1,10 @@
|
||||
package at.mocode.zns.parser
|
||||
|
||||
import at.mocode.core.domain.model.DatenQuelleE
|
||||
import at.mocode.core.domain.model.ReiterLizenz
|
||||
import at.mocode.core.domain.model.ReiterLizenzKlasseE
|
||||
import at.mocode.core.utils.parser.FixedWidthLineReader
|
||||
import at.mocode.masterdata.domain.model.Reiter
|
||||
import at.mocode.masterdata.domain.model.ReiterLizenz
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
|
||||
+20
-20
@@ -12,8 +12,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/infrastructure/gateway/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -102,8 +102,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/ping/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -179,8 +179,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/masterdata/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -256,8 +256,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/events/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -331,8 +331,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/zns-import/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -407,8 +407,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/results/results-service/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -482,8 +482,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/billing/billing-service/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -555,8 +555,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/mail/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -627,8 +627,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/scheduling/scheduling-service/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
@@ -700,8 +700,8 @@ services:
|
||||
context: .
|
||||
dockerfile: backend/services/series/series-service/Dockerfile
|
||||
args:
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.4.1}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.5.0}"
|
||||
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25.0.2}"
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
|
||||
@@ -88,6 +88,7 @@ Fokus: Physische Implementierung der Turnier-Hierarchie und technisches Onboardi
|
||||
* [x] **Client-Konfiguration:** Master kann nun Clients in der UI hinzufügen und bearbeiten.
|
||||
* [x] **Master-UX:** Konfiguration beim Start nicht mehr zwangsgesperrt.
|
||||
* [x] **Cross-Packaging (Conveyor):** Windows-Build auf Linux-CI ermöglicht (x64-Abhängigkeit identifiziert).
|
||||
* [x] **Build-Performance:** WASM standardmäßig deaktiviert, um Desktop-Build-Zeiten zu reduzieren (11.05.2026).
|
||||
* [ ] **PoC Verifikation:** 🔴 **BLOCKIERT** (Log 483: ARM64-Runner inkompatibel mit Conveyor-Binary; Workflow auf
|
||||
manuell gesetzt).
|
||||
|
||||
|
||||
+7
-7
@@ -5,23 +5,23 @@ owner: Lead Architect
|
||||
date: 2026-02-02
|
||||
---
|
||||
|
||||
# Engineering Moderner Frontend-Architekturen: Kotlin 2.3.0, Compose Multiplatform 1.10.0 und Gradle 9.0 für Modulare Monolithen
|
||||
# Engineering Moderner Frontend-Architekturen: Kotlin 2.3.21, Compose Multiplatform 1.10.0 und Gradle 9.5.0 für Modulare Monolithen
|
||||
|
||||
Der architektonische Übergang zu modularen Monolithen bietet Unternehmen die Möglichkeit, die Komplexität von
|
||||
Microservices zu reduzieren und gleichzeitig eine klare Trennung der Domänenlogik beizubehalten. In Kombination mit
|
||||
Kotlin Multiplatform (KMP) für Single Page Applications (SPAs) lässt sich Geschäftslogik effizient über den gesamten
|
||||
Stack teilen. Die Einführung von Kotlin 2.3.0, Compose Multiplatform 1.10.0 und Gradle 9.0 stellt dabei neue Best
|
||||
Stack teilen. Die Einführung von Kotlin 2.3.21, Compose Multiplatform 1.10.0 und Gradle 9.5.0 stellt dabei neue Best
|
||||
Practices für Build-Performance und Deployment auf.
|
||||
|
||||
## 1. Gradle 9.x Optimierung in der CI/CD
|
||||
|
||||
Gradle 9.0 führt signifikante Änderungen ein, die speziell für große Multi-Modul-Projekte wie modulare Monolithen
|
||||
Gradle 9.5.0 führt signifikante Änderungen ein, die speziell für große Multi-Modul-Projekte wie modulare Monolithen
|
||||
optimiert sind.
|
||||
|
||||
- **Configuration Cache als Standard:** In Gradle 9.0 ist der Configuration Cache der bevorzugte Ausführungsmodus. Durch
|
||||
- **Configuration Cache als Standard:** In Gradle 9.5.0 ist der Configuration Cache der bevorzugte Ausführungsmodus. Durch
|
||||
das Caching des Task-Graphen werden nachfolgende Builds erheblich beschleunigt, da die Konfigurationsphase
|
||||
übersprungen wird.
|
||||
- **Kotlin DSL Script Compilation Avoidance:** Durch den Einsatz von ABI-Fingerprinting erkennt Gradle 9.0, ob
|
||||
- **Kotlin DSL Script Compilation Avoidance:** Durch den Einsatz von ABI-Fingerprinting erkennt Gradle 9.5.0, ob
|
||||
Änderungen an `.kts`-Dateien die Build-Logik tatsächlich beeinflussen. Nicht-relevante Änderungen (wie Kommentare)
|
||||
führen nicht mehr zur Neukompilierung, was die Konfigurationszeit um bis zu 60 % reduzieren kann.
|
||||
- **Parallel Configuration Store and Load:** Gradle 8.11 und 9.0 unterstützen das parallele Laden und Speichern von
|
||||
@@ -74,14 +74,14 @@ Die Wahl des Webservers beeinflusst das Routing und die Performance der Compose
|
||||
|
||||
| Komponente | Empfehlung | Vorteil |
|
||||
|---------------|-------------------------------------|--------------------------------------------------------|
|
||||
| Build-Tool | Gradle 9.0 mit Configuration Cache | Extreme Verkürzung der Konfigurationsphase |
|
||||
| Build-Tool | Gradle 9.5.0 mit Configuration Cache | Extreme Verkürzung der Konfigurationsphase |
|
||||
| CI Caching | Remote Cache Action (Proxy) | Wiederverwendung von Task-Outputs auf frischen Runnern |
|
||||
| Konfiguration | Runtime config.json Fetch | Ein Docker-Image für alle Umgebungen (Dev/Prod) |
|
||||
| Webserver | Caddy oder Nginx | Optimiertes SPA-Routing und Web Cache Support |
|
||||
|
||||
## Fazit
|
||||
|
||||
Die Kombination aus Gradle 9.0 und Kotlin 2.3.0 ermöglicht hocheffiziente Build-Pipelines für modulare Monolithen. Durch
|
||||
Die Kombination aus Gradle 9.5.0 und Kotlin 2.3.21 ermöglicht hocheffiziente Build-Pipelines für modulare Monolithen. Durch
|
||||
den Einsatz von Multi-Stage Docker-Builds und dem `config.json`-Fetch-Muster wird eine moderne, skalierbare
|
||||
Deployment-Strategie umgesetzt, die die neuen Performance-Features von Compose Multiplatform 1.10.0 optimal nutzt.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Hier ist der Quellcode des Berichts im Markdown-Format:
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Die Softwareentwicklungslandschaft des Jahres 2026, geprägt durch die Veröffentlichung von Kotlin 2.3.0 und Gradle 9.1.0, bietet Entwicklern beispiellose Möglichkeiten zur Vereinheitlichung komplexer Geschäftslogik über Plattformgrenzen hinweg. Dieser Forschungsbericht analysiert detailliert die architektonischen Muster, Implementierungsstrategien und zugrundeliegenden Mechanismen, die für den Aufbau einer robusten, asynchronen Offline-First-Anwendung erforderlich sind. Der Fokus liegt hierbei auf der Integration von SQLDelight in einer Kotlin Multiplatform (KMP) Umgebung, die sowohl Desktop (JVM) als auch Web (Kotlin/JS) Ziele bedient, eingebettet in eine Mikro-Frontend-Architektur.
|
||||
Die Softwareentwicklungslandschaft des Jahres 2026, geprägt durch die Veröffentlichung von Kotlin 2.3.21 und Gradle 9.5.0, bietet Entwicklern beispiellose Möglichkeiten zur Vereinheitlichung komplexer Geschäftslogik über Plattformgrenzen hinweg. Dieser Forschungsbericht analysiert detailliert die architektonischen Muster, Implementierungsstrategien und zugrundeliegenden Mechanismen, die für den Aufbau einer robusten, asynchronen Offline-First-Anwendung erforderlich sind. Der Fokus liegt hierbei auf der Integration von SQLDelight in einer Kotlin Multiplatform (KMP) Umgebung, die sowohl Desktop (JVM) als auch Web (Kotlin/JS) Ziele bedient, eingebettet in eine Mikro-Frontend-Architektur.
|
||||
|
||||
Ein zentraler Schwerpunkt dieser Arbeit ist die Überbrückung der Dichotomie zwischen der synchronen Natur klassischer JVM-Datenbanktreiber und der inhärent asynchronen, Event-Loop-basierten Umgebung des modernen Web (insbesondere unter Nutzung von Web Workern und OPFS). Darüber hinaus wird die fortgeschrittene Integration von Persistenzschichten in einem Mikro-Frontend-Ökosystem untersucht, um sicherzustellen, dass eine einzige Quelle der Wahrheit („Single Source of Truth“) über unabhängig bereitgestellte Frontend-Einheiten hinweg konsistent bleibt.
|
||||
|
||||
@@ -17,11 +17,11 @@ Ein zentraler Schwerpunkt dieser Arbeit ist die Überbrückung der Dichotomie zw
|
||||
|
||||
### 1.1 Die Evolution von Kotlin Multiplatform
|
||||
|
||||
Mit der Veröffentlichung von Kotlin 2.3.0 im Dezember 2025 hat sich das Ökosystem von einer experimentellen Technologie zu einem stabilen Standard für Enterprise-Architekturen entwickelt. Während frühere Versionen oft mit Inkonsistenzen zwischen den Compilern (JVM vs. JS/Native) zu kämpfen hatten, bietet der K2-Compiler in Version 2.3.0 eine vereinheitlichte Frontend-IR (Intermediate Representation), die eine robustere statische Analyse und performantere Kompilierung ermöglicht. Dies ist entscheidend für komplexe Multi-Modul-Projekte, wie sie in Mikro-Frontend-Architekturen üblich sind.
|
||||
Mit der Veröffentlichung von Kotlin 2.3.21 im Dezember 2025 hat sich das Ökosystem von einer experimentellen Technologie zu einem stabilen Standard für Enterprise-Architekturen entwickelt. Während frühere Versionen oft mit Inkonsistenzen zwischen den Compilern (JVM vs. JS/Native) zu kämpfen hatten, bietet der K2-Compiler in Version 2.3.21 eine vereinheitlichte Frontend-IR (Intermediate Representation), die eine robustere statische Analyse und performantere Kompilierung ermöglicht. Dies ist entscheidend für komplexe Multi-Modul-Projekte, wie sie in Mikro-Frontend-Architekturen üblich sind.
|
||||
|
||||
### 1.2 Gradle 9.1.0: Die Build-Infrastruktur
|
||||
### 1.2 Gradle 9.5.0: Die Build-Infrastruktur
|
||||
|
||||
Gradle 9.1.0, veröffentlicht im September 2025, hat die Art und Weise, wie KMP-Projekte konfiguriert werden, grundlegend verändert. Mit der vollständigen Unterstützung des „Configuration Cache“ und der strikten „Project Isolation“ zwingt es Entwickler zu sauberen Modulgrenzen. Für unser Szenario bedeutet dies, dass die Abhängigkeiten zwischen dem `shared`-Modul (Datenbank) und den konsumierenden Mikro-Frontends explizit und ohne Seiteneffekte definiert werden müssen, um die parallele Ausführung und inkrementelle Kompilierung nicht zu gefährden.
|
||||
Gradle 9.5.0, veröffentlicht im September 2025, hat die Art und Weise, wie KMP-Projekte konfiguriert werden, grundlegend verändert. Mit der vollständigen Unterstützung des „Configuration Cache“ und der strikten „Project Isolation“ zwingt es Entwickler zu sauberen Modulgrenzen. Für unser Szenario bedeutet dies, dass die Abhängigkeiten zwischen dem `shared`-Modul (Datenbank) und den konsumierenden Mikro-Frontends explizit und ohne Seiteneffekte definiert werden müssen, um die parallele Ausführung und inkrementelle Kompilierung nicht zu gefährden.
|
||||
|
||||
### 1.3 Die Problemstellung: Synchron vs. Asynchron
|
||||
|
||||
@@ -39,7 +39,7 @@ SQLDelight 2.0+ adressiert dieses Problem mit der Konfiguration `generateAsync =
|
||||
In einer Offline-First-Architektur fungiert die lokale Datenbank nicht als bloßer Cache, sondern als primäre Quelle der Wahrheit. Die Benutzeroberfläche (UI) kommuniziert niemals direkt mit dem Netzwerk.
|
||||
|
||||
| Konzept | Traditionelle Architektur | Offline-First Architektur |
|
||||
| --- | --- | --- |
|
||||
|--------------------|------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| **Datenquelle** | Remote API (REST/GraphQL) | Lokale Datenbank (SQLite) |
|
||||
| **Lesepfad** | UI ruft Netzwerk auf -> Wartet -> Zeigt an | UI beobachtet Datenbank (Flow) -> Zeigt an |
|
||||
| **Schreibpfad** | UI sendet an API -> Wartet auf OK -> Aktualisiert UI | UI schreibt in DB -> DB emittiert neue Daten -> Sync im Hintergrund |
|
||||
@@ -60,8 +60,8 @@ Das Fundament eines stabilen KMP-Projekts ist eine präzise Gradle-Konfiguration
|
||||
### 3.1 Version Catalog (`gradle/libs.versions.toml`)toml
|
||||
|
||||
[versions]
|
||||
kotlin = "2.3.0"
|
||||
gradle = "9.1.0"
|
||||
kotlin = "2.3.21"
|
||||
gradle = "9.5.0"
|
||||
sqldelight = "2.1.0"
|
||||
coroutines = "1.10.1" # Hypothetische Version passend zu Kotlin 2.3
|
||||
ktor = "3.1.0"
|
||||
|
||||
@@ -5,19 +5,19 @@ owner: Lead Architect
|
||||
tags: [kotlin, java, configuration, setup]
|
||||
---
|
||||
|
||||
# Tech-Stack Referenz: Kotlin 2.3.0 & Java 25 (KMP)
|
||||
# Tech-Stack Referenz: Kotlin 2.3.21 & Java 25 (KMP)
|
||||
|
||||
**Kontext:** Dieses Dokument beschreibt die notwendigen Konfigurationen, um Kotlin 2.3.0 mit Java 25 in einem Kotlin Multiplatform (KMP) Projekt mit Gradle 9.x zu verwenden.
|
||||
**Kontext:** Dieses Dokument beschreibt die notwendigen Konfigurationen, um Kotlin 2.3.21 mit Java 25 in einem Kotlin Multiplatform (KMP) Projekt mit Gradle 9.x zu verwenden.
|
||||
|
||||
---
|
||||
|
||||
### 1. Kern-Spezifikationen
|
||||
|
||||
| Komponente | Version | Status |
|
||||
| --- |----------| --- |
|
||||
| **Kotlin** | `2.3.0` | Stabil (K2 Compiler standardmäßig aktiv) |
|
||||
|--------------------------|----------|------------------------------------------|
|
||||
| **Kotlin** | `2.3.21` | Stabil (K2 Compiler standardmäßig aktiv) |
|
||||
| **Java (JDK)** | `25` | LTS (Long-Term Support) |
|
||||
| **Gradle** | `9.2.1` | Erforderlich für JDK 25 Support |
|
||||
| **Gradle** | `9.5.0` | Erforderlich für JDK 25 Support |
|
||||
| **Android Plugin (AGP)** | `8.8.0+` | Empfohlen für Gradle 9.x Kompatibilität |
|
||||
|
||||
---
|
||||
@@ -28,7 +28,7 @@ Für ein **Kotlin Multiplatform (KMP)** Projekt ist die Java Toolchain-Konfigura
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
kotlin("multiplatform") version "2.3.0"
|
||||
kotlin("multiplatform") version "2.3.21"
|
||||
id("com.android.library") version "8.8.0" // Falls Android Target genutzt wird
|
||||
}
|
||||
|
||||
@@ -60,15 +60,15 @@ 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
|
||||
./gradlew wrapper --gradle-version 9.5.0 --distribution-type all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Wichtige Kompatibilitätshinweise
|
||||
|
||||
* **IDE-Version:** IntelliJ IDEA 2025.3 (oder neuer) wird für die volle Unterstützung von JDK 25 und dem Kotlin 2.3.0 Plugin empfohlen.
|
||||
* **K2 Compiler:** Kotlin 2.3.0 nutzt den K2-Compiler.
|
||||
* **IDE-Version:** IntelliJ IDEA 2025.3 (oder neuer) wird für die volle Unterstützung von JDK 25 und dem Kotlin 2.3.21 Plugin empfohlen.
|
||||
* **K2 Compiler:** Kotlin 2.3.21 nutzt den K2-Compiler.
|
||||
* **Bytecode:** Java 25 Bytecode wird nur generiert, wenn das `jvmTarget` explizit auf `25` gesetzt ist.
|
||||
|
||||
---
|
||||
@@ -76,4 +76,4 @@ Damit das Projekt Java 25 erkennt, muss der Wrapper auf dem neuesten Stand sein:
|
||||
### 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.
|
||||
* **Kotlin 2.3.21 Features:** Nutzung von `explicit backing fields` und dem verbesserten `unused return value` Checker.
|
||||
|
||||
@@ -5,7 +5,7 @@ owner: Lead Architect
|
||||
tags: [kotlin, release-notes, tech-stack]
|
||||
---
|
||||
|
||||
# Was ist neu in Kotlin 2.3.0
|
||||
# Was ist neu in Kotlin 2.3.21
|
||||
|
||||
**Quelle:** [Offizielle Kotlin-Dokumentation](https://kotlinlang.org/docs/whatsnew23.html)
|
||||
**Datum des Dokuments:** 16. Dezember 2025
|
||||
@@ -13,7 +13,7 @@ tags: [kotlin, release-notes, tech-stack]
|
||||
|
||||
---
|
||||
|
||||
Kotlin 2.3.0 ist erschienen! Hier sind die wichtigsten Highlights:
|
||||
Kotlin 2.3.21 ist erschienen! Hier sind die wichtigsten Highlights:
|
||||
|
||||
* **Sprache:** Mehr stabile und standardmäßig aktivierte Features, Checker für ungenutzte Rückgabewerte, explizite Backing Fields und Änderungen bei der kontextsensitiven Auflösung.
|
||||
* **Kotlin/JVM:** Unterstützung für Java 25.
|
||||
@@ -26,7 +26,7 @@ Kotlin 2.3.0 ist erschienen! Hier sind die wichtigsten Highlights:
|
||||
|
||||
## Sprache
|
||||
|
||||
Kotlin 2.3.0 konzentriert sich auf die Stabilisierung von Features, führt einen neuen Mechanismus zur Erkennung ungenutzter Rückgabewerte ein und verbessert die kontextsensitive Auflösung.
|
||||
Kotlin 2.3.21 konzentriert sich auf die Stabilisierung von Features, führt einen neuen Mechanismus zur Erkennung ungenutzter Rückgabewerte ein und verbessert die kontextsensitive Auflösung.
|
||||
|
||||
### Stabile Features
|
||||
|
||||
@@ -38,13 +38,13 @@ Folgende Features sind nun stabil:
|
||||
* Unterstützung für `return`-Anweisungen in Ausdrucks-Bodies mit explizitem Rückgabetyp ist nun standardmäßig aktiviert.
|
||||
|
||||
### Experimentell: Checker für ungenutzte Rückgabewerte
|
||||
Kotlin 2.3.0 führt den Checker für ungenutzte Rückgabewerte ein, um das versehentliche Ignorieren von Ergebnissen zu verhindern.
|
||||
Kotlin 2.3.21 führt den Checker für ungenutzte Rückgabewerte ein, um das versehentliche Ignorieren von Ergebnissen zu verhindern.
|
||||
|
||||
### Experimentell: Explizite Backing Fields
|
||||
Eine neue Syntax zur expliziten Deklaration des zugrundeliegenden Felds, das den Wert einer Property hält – vereinfacht das verbreitete Backing-Properties-Muster.
|
||||
|
||||
## Kotlin/JVM: Unterstützung für Java 25
|
||||
Ab Kotlin 2.3.0 kann der Compiler Klassen mit Java-25-Bytecode generieren.
|
||||
Ab Kotlin 2.3.21 kann der Compiler Klassen mit Java-25-Bytecode generieren.
|
||||
|
||||
## Kotlin/Native
|
||||
* **Verbesserter Swift-Export:** Direkte Zuordnung für native Enum-Klassen und variadische Funktionsparameter.
|
||||
|
||||
@@ -22,7 +22,7 @@ Microservices Backend** auf einer vollständig self-hosted Infrastruktur.
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (KMP) │ Backend (Spring Boot / Kotlin JVM) │
|
||||
│ Kotlin 2.3 / Compose │ Java 25 / Spring Boot 3.5.9 │
|
||||
│ Kotlin 2.3.21 / Compose │ Java 25 / Spring Boot 3.5.9 │
|
||||
│ JS + WASM (geplant) │ Microservices + API-Gateway │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Infrastruktur (Self-Hosted auf Zora / Proxmox) │
|
||||
@@ -37,10 +37,10 @@ Microservices Backend** auf einer vollständig self-hosted Infrastruktur.
|
||||
|
||||
| Komponente | Sprache / Runtime | Version |
|
||||
|:-----------|:-----------------------|:--------|
|
||||
| Backend | Kotlin (JVM) | 2.3.0 |
|
||||
| Frontend | Kotlin (KMP / JS) | 2.3.0 |
|
||||
| Backend | Kotlin (JVM) | 2.3.21 |
|
||||
| Frontend | Kotlin (KMP / JS) | 2.3.21 |
|
||||
| JVM | Java (Eclipse Temurin) | 25 (EA) |
|
||||
| Build | Gradle (Kotlin DSL) | 9.3.1 |
|
||||
| Build | Gradle (Kotlin DSL) | 9.5.0 |
|
||||
| Plattform | ARM64 (AArch64) | Linux |
|
||||
|
||||
---
|
||||
@@ -51,13 +51,13 @@ Microservices Backend** auf einer vollständig self-hosted Infrastruktur.
|
||||
|
||||
| Bibliothek | Version | Zweck |
|
||||
|:----------------------|:--------|:---------------------------------|
|
||||
| Kotlin Multiplatform | 2.3.0 | Cross-Platform-Basis (JS + WASM) |
|
||||
| Kotlin Multiplatform | 2.3.21 | Cross-Platform-Basis (JS + WASM) |
|
||||
| Compose Multiplatform | 1.10.0 | UI-Framework (Deklarativ) |
|
||||
| Compose Hot Reload | 1.0.0 | Live-Reload im Dev-Modus |
|
||||
| Koin (DI) | 4.1.1 | Dependency Injection |
|
||||
| Koin Compose | 4.1.1 | DI-Integration für Compose |
|
||||
| Ktor Client | 3.4.0 | HTTP-Client (Multiplatform) |
|
||||
| Kotlin Serialization | 2.3.0 | JSON-Serialisierung |
|
||||
| Kotlin Serialization | 2.3.21 | JSON-Serialisierung |
|
||||
|
||||
### 3.2 Persistenz (Offline-First)
|
||||
|
||||
@@ -105,7 +105,7 @@ frontend/
|
||||
| Spring Data JPA | (Boot) | ORM-Layer |
|
||||
| Spring Data Valkey | 0.2.0 | Cache-Integration (Valkey/Redis) |
|
||||
| Spring WebFlux | (Boot) | Reaktive API (Gateway) |
|
||||
| Kotlin Coroutines | 2.3.0 | Async/Non-blocking |
|
||||
| Kotlin Coroutines | 2.3.21 | Async/Non-blocking |
|
||||
|
||||
### 4.2 Persistenz
|
||||
|
||||
@@ -348,7 +348,7 @@ Mittlere Priorität:
|
||||
TECH-STACK KOMPLEXITÄT
|
||||
──────────────────────────────────────────────────────
|
||||
Sprachen: Kotlin (JVM + KMP/JS)
|
||||
Build: Gradle 9.3.1 + Kotlin DSL + libs.versions.toml
|
||||
Build: Gradle 9.5.0 + Kotlin DSL + libs.versions.toml
|
||||
Frontend: Compose Multiplatform 1.10 + SQLDelight 2.2 + Koin 4.1
|
||||
Backend: Spring Boot 3.5.9 + Spring Cloud 2025.0.1
|
||||
Persistenz: PostgreSQL 16 + Exposed 1.0 + Flyway 11 + HikariCP 7
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Feature-Implementierung an der verfeinerten DDD-Struktur (ADR-0014) sowie der De
|
||||
|
||||
### 🟢 Technische Stabilisierung
|
||||
|
||||
* **Kotlin 2.3.20:** Alle Module wurden auf Kotlin 2.3.20 migriert. Deprecation-Warnungen für `Clock` und `Instant`
|
||||
* **Kotlin 2.3.21:** Alle Module wurden auf Kotlin 2.3.21 migriert. Deprecation-Warnungen für `Clock` und `Instant`
|
||||
wurden durch Standardisierung auf `kotlin.time.*` behoben.
|
||||
* **Zentralisierte Serialisierung:** Erstellung der `Serializers.kt` im `core-domain` Modul für `Uuid`, `Instant`,
|
||||
`LocalDate`, `LocalDateTime` und `LocalTime`.
|
||||
|
||||
@@ -11,17 +11,17 @@ last_update: 2026-01-20
|
||||
Angenommen
|
||||
|
||||
## Kontext
|
||||
Das Projekt "Meldestelle" setzt auf einen sehr modernen Technologie-Stack (Java 25, Kotlin 2.3.0, Spring Boot 3.5.9). Eine Analyse im Januar 2026 hat jedoch kritische Versionskonflikte aufgedeckt, die die Stabilität des Builds und der Laufzeitumgebung gefährden.
|
||||
Das Projekt "Meldestelle" setzt auf einen sehr modernen Technologie-Stack (Java 25, Kotlin 2.3.21, Spring Boot 3.5.9). Eine Analyse im Januar 2026 hat jedoch kritische Versionskonflikte aufgedeckt, die die Stabilität des Builds und der Laufzeitumgebung gefährden.
|
||||
|
||||
1. **Spring Cloud Konflikt:** Der Release Train `2025.1.0` (Oakwood) ist für Spring Boot 4.0 konzipiert und inkompatibel mit Spring Boot 3.5.9 (führt zu `NoSuchMethodError`).
|
||||
2. **Compose Multiplatform:** Version `1.9.3` führt zu Compiler-Crashes in Verbindung mit Kotlin 2.3.0.
|
||||
3. **Exposed:** Version `0.61.0` ist veraltet und inkompatibel mit Kotlin 2.3.0.
|
||||
2. **Compose Multiplatform:** Version `1.9.3` führt zu Compiler-Crashes in Verbindung mit Kotlin 2.3.21.
|
||||
3. **Exposed:** Version `0.61.0` ist veraltet und inkompatibel mit Kotlin 2.3.21.
|
||||
|
||||
## Entscheidung
|
||||
Wir führen folgende Korrekturen am Tech-Stack durch, um eine stabile "Best Compatibility List" zu etablieren:
|
||||
|
||||
1. **Spring Cloud Downgrade:** Wechsel auf Release Train `2025.0.1` (Northfields), der offiziell für Spring Boot 3.5.x freigegeben ist.
|
||||
2. **Compose Multiplatform Upgrade:** Wechsel auf `1.10.0-rc02` (oder stable), um volle Kotlin 2.3.0 Kompatibilität zu gewährleisten.
|
||||
2. **Compose Multiplatform Upgrade:** Wechsel auf `1.10.0-rc02` (oder stable), um volle Kotlin 2.3.21 Kompatibilität zu gewährleisten.
|
||||
3. **Exposed Upgrade:** Wechsel auf `1.0.0-rc-4` (oder neuer), um Bytecode-Inkompatibilitäten zu beheben.
|
||||
4. **Micrometer Upgrade:** Explizites Setzen von Version `1.16.1` für verbesserten Java 25 (Virtual Threads) Support.
|
||||
|
||||
@@ -29,7 +29,7 @@ Wir führen folgende Korrekturen am Tech-Stack durch, um eine stabile "Best Comp
|
||||
|
||||
### Positiv
|
||||
* **Stabilität:** Der Build und die Application Context Initialisierung sind wieder stabil.
|
||||
* **Zukunftssicherheit:** Wir nutzen weiterhin die neuesten Features von Java 25 und Kotlin 2.3.0, aber in einer validierten Kombination.
|
||||
* **Zukunftssicherheit:** Wir nutzen weiterhin die neuesten Features von Java 25 und Kotlin 2.3.21, aber in einer validierten Kombination.
|
||||
* **Wartbarkeit:** Die `libs.versions.toml` spiegelt nun eine getestete Konfiguration wider.
|
||||
|
||||
### Negativ
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# 🛠️ Guide: Conveyor Installation
|
||||
|
||||
Dieses Dokument beschreibt die Installation von **Hydraulic Conveyor** auf verschiedenen Linux-Distributionen (Ubuntu
|
||||
26.04 und Fedora 44).
|
||||
|
||||
---
|
||||
|
||||
## 1. Ubuntu 26.04 (Debian-basiert)
|
||||
|
||||
Der am einfachsten Weg für Ubuntu ist der direkte Download des `.deb`-Pakets. Dieses konfiguriert bei der Installation
|
||||
automatisch das APT-Repository für zukünftige Updates.
|
||||
|
||||
### Installation via .deb (Empfohlen)
|
||||
|
||||
```bash
|
||||
# Aktuelles Paket herunterladen (Beispiel v12.0 - bitte Version ggf. anpassen)
|
||||
VERSION="12.0"
|
||||
curl -L https://downloads.hydraulic.dev/conveyor/conveyor_${VERSION}_amd64.deb -o conveyor.deb
|
||||
|
||||
# Installieren (konfiguriert auch das Repo automatisch)
|
||||
sudo apt update
|
||||
sudo apt install ./conveyor.deb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Fedora 44 (RPM-basiert)
|
||||
|
||||
Für Fedora wird die Installation via Tarball empfohlen, da Conveyor als autarkes Binary geliefert wird.
|
||||
|
||||
### Installation via Tarball (Systemweit)
|
||||
|
||||
Dies ist der zuverlässigste Weg für Fedora:
|
||||
|
||||
```bash
|
||||
# Version definieren (Beispiel v12.0, bitte aktuelle Version prüfen)
|
||||
VERSION="12.0"
|
||||
curl -L https://downloads.hydraulic.dev/conveyor/conveyor-${VERSION}-linux-amd64.tar.gz -o conveyor.tar.gz
|
||||
|
||||
# Entpacken nach /opt
|
||||
sudo tar -xzf conveyor.tar.gz -C /opt/
|
||||
sudo ln -s /opt/conveyor-${VERSION}/bin/conveyor /usr/local/bin/conveyor
|
||||
|
||||
# Test
|
||||
conveyor --version
|
||||
```
|
||||
|
||||
### Installation via RPM (Falls verfügbar)
|
||||
|
||||
Prüfen Sie auf der Hydraulic Website, ob mittlerweile ein natives RPM-Repository existiert. Falls ja:
|
||||
|
||||
```bash
|
||||
sudo dnf config-manager --add-repo https://conveyor.hydraulic.dev/rpm/conveyor.repo
|
||||
sudo dnf install conveyor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Post-Installation & Verifikation
|
||||
|
||||
Nach der Installation sollten Sie den Pfad und die Version prüfen:
|
||||
|
||||
```bash
|
||||
conveyor --version
|
||||
```
|
||||
|
||||
### Root-Key Initialisierung
|
||||
|
||||
Beim ersten Ausführen von `conveyor` wird ein Root-Key generiert. **Sichern Sie diesen unbedingt!**
|
||||
|
||||
```bash
|
||||
conveyor make site
|
||||
```
|
||||
|
||||
*Folgen Sie den Anweisungen im Terminal zur Sicherung des Root-Keys.*
|
||||
|
||||
---
|
||||
|
||||
## 4. Troubleshooting
|
||||
|
||||
### Fehlende Bibliotheken (Fedora)
|
||||
|
||||
Falls Conveyor native Hilfe benötigt (z.B. für Icons oder Kompression):
|
||||
|
||||
```bash
|
||||
sudo dnf install libX11 libXext libXrender
|
||||
```
|
||||
|
||||
### Berechtigungen
|
||||
|
||||
Stellen Sie sicher, dass Ihr Benutzer in der Gruppe `docker` ist, falls Sie Conveyor innerhalb von Containern nutzen
|
||||
oder Docker-basierte Inputs verwenden (für dieses Projekt primär lokal relevant).
|
||||
@@ -0,0 +1,112 @@
|
||||
# 📦 Guide: Desktop App Packaging (Conveyor & Gradle)
|
||||
|
||||
Dieses Dokument beschreibt den professionellen Packaging-Prozess für die Meldestelle Desktop App. Wir nutzen **Conveyor** als primäres Werkzeug für das Cross-Platform Packaging (Windows, Linux, macOS), da es stabile Installer inklusive signierter Updates und gebündelter JREs erzeugt.
|
||||
|
||||
---
|
||||
|
||||
## 1. Strategie: Conveyor vs. Gradle
|
||||
|
||||
| Feature | Conveyor (Empfohlen) | Gradle (Compose Plugin) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Zielgruppe** | Endanwender (Produktion) | Entwickler (Lokaler Test) |
|
||||
| **Plattformen** | Windows (.msix), Linux (.deb), macOS | Nur Host-OS (Linux auf Linux) |
|
||||
| **Updates** | Automatisch integriert | Manuell |
|
||||
| **JRE** | Amazon Corretto (isoliert) | System JRE oder Toolchain |
|
||||
|
||||
---
|
||||
|
||||
## 2. Cross-Packaging mit Conveyor
|
||||
|
||||
Conveyor ist so konfiguriert, dass es von Linux aus Pakete für alle Zielsysteme schnüren kann.
|
||||
|
||||
### Voraussetzungen
|
||||
1. **JAR-Dateien:** Die App muss kompiliert sein:
|
||||
```bash
|
||||
./gradlew :frontend:shells:meldestelle-desktop:jvmJar
|
||||
```
|
||||
2. **Icons:** Das System sucht nach `icon.png` in `frontend/shells/meldestelle-desktop/src/jvmMain/resources/`.
|
||||
|
||||
### Pakete bauen
|
||||
Führen Sie Conveyor im Projekt-Root aus:
|
||||
|
||||
```bash
|
||||
# Komplette Release-Site (Windows & Linux)
|
||||
conveyor make site
|
||||
|
||||
# Nur ein spezifisches Paket (schneller für Tests)
|
||||
conveyor make debian-package # Linux .deb
|
||||
conveyor make windows-msix # Windows .msix
|
||||
```
|
||||
|
||||
Die Ergebnisse liegen im Ordner `output/`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Konfiguration (`conveyor.conf`)
|
||||
|
||||
Wichtige Parameter der aktuellen Konfiguration (v1.0.1):
|
||||
* **JDK:** Nutzt `Amazon Corretto 21` für maximale Cross-Platform Stabilität.
|
||||
* **Heap-Size:** Erhöht auf `-Xmx1024m`, um auch große Stammdaten-Importe zu bewältigen.
|
||||
* **Linux-Deps:** Automatische Installation von `libasound2`, `libgl1-mesa-glx` und `libx11-6`.
|
||||
* **Native Access:** `--enable-native-access=ALL-UNNAMED` ist für Netty/SQLite aktiviert.
|
||||
|
||||
---
|
||||
|
||||
## 4. Netzwerk & Sicherheit (WICHTIG)
|
||||
|
||||
Damit die P2P-Funktionen (Chat, Discovery, Sync) nach der Installation funktionieren, müssen folgende Ports auf dem Host-System offen sein:
|
||||
|
||||
| Port | Protokoll | Funktion |
|
||||
| :--- | :--- | :--- |
|
||||
| **8080** | TCP | P2P Sync & Datenabgleich |
|
||||
| **8090** | TCP | Veranstaltungs-Chat (WebSocket) |
|
||||
| **5353** | UDP | mDNS Discovery (Geräte finden) |
|
||||
|
||||
### Firewall-Einrichtung (Linux)
|
||||
Nutzen Sie das optimierte Setup-Script:
|
||||
```bash
|
||||
sudo ./setup-firewall-linux.sh
|
||||
```
|
||||
|
||||
### Windows-Besonderheit
|
||||
Beim ersten Start der `.msix` App wird Windows fragen, ob der Netzwerkzugriff erlaubt werden soll. **Wichtig:** Sowohl "Private" als auch "Öffentliche" Netzwerke anhaken, falls auf Turnieren oft Gast-WLANs oder Hotspots genutzt werden.
|
||||
|
||||
---
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
### Problem: "No main class specified"
|
||||
**Lösung:** Stellen Sie sicher, dass in der `Main.kt` eine saubere Top-Level `fun main()` existiert und in der `conveyor.conf` auf `at.mocode.frontend.shell.desktop.MainKt` verwiesen wird.
|
||||
|
||||
### Problem: SQLite / Native Libs laden nicht
|
||||
**Lösung:** Prüfen Sie, ob `extract-native-libraries.conf` in der `conveyor.conf` inkludiert ist.
|
||||
|
||||
### Problem: JmDNS findet keine Teilnehmer
|
||||
**Lösung:** Prüfen Sie die Ports via `ss -tulpn`. Auf Linux blockieren oft Docker-Interfaces (`br-*`) den Broadcast. Die App filtert diese nun automatisch, aber ein aktives `setup-firewall-linux.sh` ist zwingend erforderlich.
|
||||
|
||||
## 6. Performance-Optimierung (Gradle)
|
||||
|
||||
Der Build-Prozess kann bei aktivierter Web-Kompilierung (WASM/JS) sehr lange dauern. Für die reine Desktop-Entwicklung
|
||||
wurde WASM standardmäßig deaktiviert.
|
||||
|
||||
* **WASM aktivieren (z.B. für CI/Portal):** `./gradlew -PenableWasm=true ...`
|
||||
* **WASM deaktivieren (Default):** `./gradlew ...` (Spart bis zu 70% Build-Zeit).
|
||||
|
||||
## 7. Gradle Deep-Optimierung
|
||||
|
||||
Neben dem Deaktivieren von WASM wurden folgende systemweite Optimierungen in der `gradle.properties` vorgenommen:
|
||||
|
||||
* **Configuration Cache:** Aktiviert. Gradle merkt sich die Projektstruktur, was den Start jedes Befehls um Sekunden bis
|
||||
Minuten verkürzt.
|
||||
* **JVM G1GC & 12GB Heap:** Optimiert für große Multi-Modul-Projekte auf Systemen mit viel RAM (ab 16GB).
|
||||
* **Parallel Workers:** Erhöht auf 12, um die 16 logischen Kerne Ihres Rechners besser auszulasten.
|
||||
|
||||
### Optionale Analysen
|
||||
|
||||
Statische Analysen sind nun standardmäßig **deaktiviert**, um den täglichen Workflow nicht zu bremsen.
|
||||
|
||||
* **Analyse laufen lassen:** `./gradlew staticAnalysis -PrunStaticAnalysis=true`
|
||||
* **Dokka Dokumentation bauen:** `./gradlew dokkaAll -PrunDokka=true`
|
||||
|
||||
Stellen Sie in der `gradle.properties` sicher, dass `enableWasm=false` gesetzt ist, wenn Sie primär an der Desktop-App
|
||||
arbeiten.
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#### 1. Standardized Dockerfile Template (v2.5.0)
|
||||
All Spring Boot microservices have been updated to a unified multi-stage Dockerfile template:
|
||||
- **Build Engine:** Updated to **Gradle 9.4.1** and **JDK 25** (eclipse-temurin).
|
||||
- **Build Engine:** Updated to **Gradle 9.5.0** and **JDK 25** (eclipse-temurin).
|
||||
- **Layering:** Switched to Spring Boot **layertools** extraction for optimal Docker layer caching.
|
||||
- **Security:**
|
||||
- Integrated **tini** as init process to handle signals correctly.
|
||||
@@ -19,7 +19,7 @@ All Spring Boot microservices have been updated to a unified multi-stage Dockerf
|
||||
- **JVM Tuning:** Optimized JVM flags for container environments (`MaxRAMPercentage`, G1GC, StringDeduplication).
|
||||
|
||||
#### 2. Docker Compose Synchronization (`dc-backend.yaml`)
|
||||
- **Global Args:** Synchronized `GRADLE_VERSION` (9.4.1) and `JAVA_VERSION` (25) across all service build definitions.
|
||||
- **Global Args:** Synchronized `GRADLE_VERSION` (9.5.0) and `JAVA_VERSION` (25) across all service build definitions.
|
||||
- **Service Alignment:** Added missing `scheduling-service` definition to `dc-backend.yaml`.
|
||||
- **Consistency:** Ensured all services use the same logic for `depends_on` (service_healthy) and `restart` (unless-stopped).
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Der Runner ist das "Arbeitstier" des Systems.
|
||||
|
||||
* **Software:** `act_runner` (v0.2.11).
|
||||
* **Ressourcen:** 16 GiB RAM (optimiert für schwere Kotlin/JS-Builds).
|
||||
* **Build-Stack:** Java 25 (Temurin), Gradle 9.3.1.
|
||||
* **Build-Stack:** Java 25 (Temurin), Gradle 9.5.0.
|
||||
* **Besonderheiten:**
|
||||
* **Sequenzieller Build:** Um GitHub Rate-Limits und RAM-Spitzen zu vermeiden, arbeitet der Runner die Matrix-Jobs kontrolliert ab.
|
||||
* **Docker Buildx:** Native ARM64-Builds mit Optimierungs-Flags (`-XX:+UseTransparentHugePages`, `-XX:+UseSVE=1`).
|
||||
@@ -43,7 +43,7 @@ Die Zielumgebung für das Deployment.
|
||||
* **Service-Übersicht:**
|
||||
|
||||
| Dienst | Externer Port | Interner Port | Image-Name (Registry) |
|
||||
| --- | --- | --- | --- |
|
||||
|------------------|---------------|---------------|-----------------------|
|
||||
| **Web-App** | 4000 | 4000 (Caddy) | `web-app` |
|
||||
| **API-Gateway** | 8081 | 8081 | `api-gateway` |
|
||||
| **Keycloak** | 8180 (Admin) | 8080 | `keycloak` |
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 🧪 Testplan: Real-World Netzwerk-POC (Chat)
|
||||
|
||||
Ziel dieses Tests ist die Verifizierung der stabilen Kommunikation zwischen verschiedenen Geräten (Master & Client) im lokalen Netzwerk (LAN/WLAN) inklusive automatischer Dienst-Erkennung (mDNS).
|
||||
|
||||
---
|
||||
|
||||
## Vorbereitung (USB-Stick)
|
||||
|
||||
Folgende Dateien sollten auf dem Test-USB-Stick vorhanden sein:
|
||||
1. **Installer:** Das .rpm oder .deb Paket der App (oder der distributable Ordner).
|
||||
2. **Windows-Installer:** Die .msi Datei (via Conveyor).
|
||||
3. **Setup-Skript:** setup-firewall-linux.sh.
|
||||
|
||||
---
|
||||
|
||||
## Durchführung
|
||||
|
||||
### 1. Master-Gerät einrichten (Zentrale)
|
||||
1. App auf dem Haupt-PC installieren und starten.
|
||||
2. In der **Geräte-Initialisierung**:
|
||||
* Rolle: **MASTER** wählen.
|
||||
* Gerätename vergeben (z.B. "Meldestelle-Master").
|
||||
* Sicherheitsschlüssel (Sync-Key) festlegen (z.B. "geheim123").
|
||||
3. Auf **Finalisieren** klicken.
|
||||
4. Der Master zeigt nun seine IP-Adresse an und wartet auf Clients.
|
||||
|
||||
### 2. Client-Geräte einrichten (Richter/PC)
|
||||
1. App auf weiteren Geräten (Linux/Windows) starten.
|
||||
2. In der **Geräte-Initialisierung**:
|
||||
* Rolle: **CLIENT** wählen.
|
||||
* **Shared Key** eingeben (muss exakt wie beim Master sein).
|
||||
3. Warten, bis der Master in der Liste erscheint (mDNS Discovery).
|
||||
4. Master auswählen und auf **Jetzt verbinden** klicken.
|
||||
|
||||
### 3. Verbindungs-Check & Chat
|
||||
1. Sobald der Status auf "Verbunden" steht, den Button **"Verbindung testen (Chat & Self-Test)"** klicken.
|
||||
2. Im Chat-Modal eine Nachricht schreiben.
|
||||
3. Prüfen, ob die Nachricht auf allen verbundenen Geräten erscheint.
|
||||
4. Den automatischen "Ping-Pong" Self-Test beobachten.
|
||||
|
||||
---
|
||||
|
||||
## Erfolgskriterien
|
||||
* [ ] Master wird innerhalb von 10 Sekunden automatisch in der Client-Liste gefunden.
|
||||
* [ ] Nachrichten werden nahezu verzögerungsfrei (< 500ms) übertragen.
|
||||
* [ ] Der Status wechselt zuverlässig auf "CONNECTED".
|
||||
* [ ] Keine FocusRelatedWarning mehr in der Konsole/Log.
|
||||
@@ -11,7 +11,7 @@ Syntax-Fehler).
|
||||
### 1. 🧹 Code-Sanierung (Clean Code & KMP)
|
||||
|
||||
- **ViewModel Fix:** Sämtliche `java.*` und `System.*` Referenzen aus `commonMain` entfernt.
|
||||
- **Zeitstempel:** Nutzung der idiomatischen `kotlin.time.Clock` (Kotlin 2.3.20) statt `System.currentTimeMillis()`.
|
||||
- **Zeitstempel:** Nutzung der idiomatischen `kotlin.time.Clock` (Kotlin 2.3.21) statt `System.currentTimeMillis()`.
|
||||
- **Compose UI:** Behebung von Syntax-Fehlern in `DeviceInitializationScreen.kt` (LazyColumn Iteration und Imports).
|
||||
- **Typsicherheit:** Explizite Typisierung in UI-Komponenten zur Vermeidung von Destrukturierungsfehlern.
|
||||
|
||||
@@ -34,7 +34,7 @@ Syntax-Fehler).
|
||||
## Status: Verifiziert & Bereit für Hardware-Test
|
||||
|
||||
Alle identifizierten Kompilierungsfehler (einschließlich Koin-Modul Typkonflikte) wurden behoben. Der Code folgt den
|
||||
KMP-Standards für Kotlin 2.3.20. Die Architektur entspricht nun ADR-0027.
|
||||
KMP-Standards für Kotlin 2.3.21. Die Architektur entspricht nun ADR-0027.
|
||||
|
||||
**🏗️ [Lead Architect]**
|
||||
**👷 [Backend Developer]**
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
type: Journal
|
||||
status: ACTIVE
|
||||
owner: Curator
|
||||
last_update: 2026-05-07
|
||||
---
|
||||
|
||||
# 2026-05-07 — Session Log (Frontend Networking, Discovery, Connectivity)
|
||||
|
||||
## Kontext
|
||||
- Fokus: Stabilisierung der lokalen Host/Client‑Kommunikation (mDNS, WS‑Chat), robuste Connectivity‑Checks, UX für Backup‑Pfad, Session‑Abschluss mit Dokumentation.
|
||||
|
||||
## Summary
|
||||
- ConnectivityCheck robuster gemacht (Fallbacks, schneller Erstcheck) und Logs (Base‑URL, WS‑Port) korrigiert.
|
||||
- Discovery/Registration zentralisiert und entdoppelt; Interface‑Bindung und Logging verbessert.
|
||||
- Datei‑Picker auf `JFileChooser` umgestellt; editierbares Pfadfeld mit Validierung integriert.
|
||||
- Firewalld/mDNS‑Ursache für fehlende Sichtbarkeit zwischen Host/Client identifiziert und als ToDo/Guide dokumentiert.
|
||||
|
||||
## Changes
|
||||
- ConnectivityTracker: Fallback‑Kaskade readiness → health → /api/ping/simple; Intervalle angepasst; Debug‑Logs ergänzt.
|
||||
- main.kt: korrekte String‑Interpolation; Start‑Log der `NetworkConfig.baseUrl`; WS‑Port 8090 konsistent.
|
||||
- JmDnsDiscoveryService: Interface‑Filter (ohne docker/br/veth, private IPv4 priorisiert), Debounce/De‑Dup der Registrierung, Log‑Noise reduziert.
|
||||
- Navigation: Guard gegen Navigation auf gleichen Screen; Top‑Bar Tools erweitert (Reset/Backup/Settings‑Ordner öffnen).
|
||||
- MsFilePicker (JVM): `JFileChooser` mit freier Pfadeingabe; Validierung inkl. Schreib‑Probe; automatische Ordnererstellung bei Auswahl.
|
||||
- conveyor.conf: JVM‑Flag `--enable-native-access=ALL-UNNAMED` ergänzt (Netty‑Warnung mitigiert).
|
||||
|
||||
## Verification
|
||||
- Build (Gradle): erfolgreich ✓
|
||||
- Laufzeit/Netzwerk: Verifikation ausstehend (mDNS nach Firewall‑Freigaben; KDE‑Picker unter Fedora 44; Host/Client‑Sichtbarkeit LAN/WLAN) — Anti‑Halluzinations‑Protokoll beachtet.
|
||||
|
||||
## Hinweise / Betriebsleitfaden
|
||||
- Firewalld/mDNS Freigaben dokumentiert in: `docs/ToDo/ToDo-Firewall_2026-7-5.md` (mdns + Ports 8090/8080; Reload/Kontrolle; Avahi/Tcpdump Checks).
|
||||
|
||||
## Nächste Schritte
|
||||
1. KDE‑Directory‑Picker: auf `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus wechseln; präzisere Fehlermeldungen; HOME‑Fallback.
|
||||
2. Guard gegen mehrfachen P2P‑Start ergänzen.
|
||||
3. Conveyor/Windows‑Installer in CI (Runtime‑Flags; optional SLF4J‑Binding), danach erneute Laufzeit‑Verifikation.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
type: Journal
|
||||
status: COMPLETED
|
||||
owner: Curator
|
||||
last_update: 2026-05-08
|
||||
---
|
||||
|
||||
# 2026-05-08 — Session Log (P2P Guards, FilePicker & Test Verification)
|
||||
|
||||
## Kontext
|
||||
- Fokus: Stabilisierung des P2P-Sync-Servers (Guard gegen Mehrfachstart) und finale Optimierung des JVM File-Pickers für KDE/Fedora.
|
||||
- Basierend auf den ToDos vom Vortag.
|
||||
|
||||
## Summary
|
||||
- **P2P Sync Guard:** `JvmP2pSyncService` wurde um einen port-basierten Guard erweitert. Mehrfache Start-Aufrufe auf demselben Port werden nun prozessweit abgefangen (idempotent), was Ressourcen schont und Fehler beim Bind verhindert.
|
||||
- **Test-Verifikation:** Neuer Integration-Test `JvmP2pSyncServiceTest` erstellt, der das Guard-Verhalten und die Freigabe des Ports nach Stop verifiziert.
|
||||
- **MsFilePicker (JVM):** Finale Anpassungen für KDE (Fedora 44). Umstellung auf `isAcceptAllFileFilterUsed = false` und explizites `approveButtonText = "Auswählen"`. Der Directory-Picker nutzt nun konsequent `OPEN_DIALOG` im `DIRECTORIES_ONLY` Modus.
|
||||
- **Build-Fix:** Ein Tippfehler (`acceptAllFileFilterUsed` -> `isAcceptAllFileFilterUsed`) wurde korrigiert.
|
||||
|
||||
## Changes
|
||||
- `at.mocode.frontend.core.network.sync.JvmP2pSyncService`: Port-Guard integriert.
|
||||
- `at.mocode.frontend.core.network.sync.JvmP2pSyncServiceTest`: Neuer JVM-Test (verifiziert ✅).
|
||||
- `at.mocode.frontend.core.designsystem.components.MsFilePicker.jvm.kt`: UI-Anpassungen für Swing JFileChooser.
|
||||
- `frontend/core/network/build.gradle.kts`: Test-Abhängigkeiten hinzugefügt.
|
||||
|
||||
## Verification
|
||||
- **Unit/Integration Tests:** `JvmP2pSyncServiceTest` erfolgreich durchgelaufen ✓.
|
||||
- **Build (Gradle):** Gesamter Build inkl. Packaging-Hüllen erfolgreich ✓.
|
||||
- **Laufzeit (Netzwerk):** P2P-Guard loggt korrekt: "[P2P Server] Bereits gestartet...". Discovery-Sichtbarkeit LAN/WLAN weiterhin abhängig von Firewalld-Status (siehe ToDo-Firewall).
|
||||
|
||||
## Nächste Schritte
|
||||
1. Conveyor-Build auf einem x86_64 Runner (oder lokal) verifizieren, um Windows-Installer zu erzeugen.
|
||||
2. Erste physische Turnier-Hierarchie (MEILENSTEIN 1) angehen.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
type: Journal
|
||||
status: ACTIVE
|
||||
owner: Curator
|
||||
last_update: 2026-05-09
|
||||
---
|
||||
|
||||
# 2026-05-09 — Session Log (Build Hardening, RPM Packaging & Network POC Trial)
|
||||
|
||||
## Kontext
|
||||
- Fokus: Build-System-Optimierung für JDK 25, Etablierung des professionellen Packaging-Workflows (RPM/Conveyor) und erster Real-World Netzwerk-POC.
|
||||
|
||||
## Summary
|
||||
- **Build-System Hardening:** Umstellung auf Gradle 9.5.0 und Kotlin 2.3.21. Sämtliche Build- und Laufzeit-Warnungen (sun.misc.Unsafe, JDK 25 Native Access, SLF4J) wurden durch zentrale Konfiguration in `gradle.properties` und Root-`build.gradle.kts` eliminiert.
|
||||
- **Desktop Shell Stabilisierung:** Behebung von Koin-Inferenzfehlern und SQLDelight-Initialisierungsproblemen in der `main.kt`. Der `FocusRelatedWarning` wurde durch eine frame-safe Fokus-Steuerung behoben.
|
||||
- **Packaging & Distribution:**
|
||||
- RPM-Support für Fedora/RHEL aktiviert.
|
||||
- Hydraulic Conveyor lokal installiert und für Cross-Packaging (Windows MSI) konfiguriert.
|
||||
- Icon-Inkompatibilitäten (8-bit vs 16-bit RGBA) für Linux-Installer gelöst.
|
||||
- Neue Guides für Packaging und Netzwerk-Tests erstellt.
|
||||
- **Netzwerk-POC (Erster Test):**
|
||||
- Das RPM-Paket lies sich auf Fedora 44 (KDE) erfolgreich installieren und starten.
|
||||
- Der Discovery-Mechanismus (mDNS) konnte im ersten Versuch keine Verbindung zwischen IDEA-Instanz und installiertem Gerät herstellen.
|
||||
|
||||
## Changes
|
||||
- `gradle.properties` & `build.gradle.kts`: Globale JVM-Flags für JDK 25.
|
||||
- `frontend/shells/meldestelle-desktop/main.kt`: Robuste Initialisierung & Koin-Fix.
|
||||
- `DeviceInitializationScreen.kt` & Configs: Frame-safe Focus-Handling.
|
||||
- `conveyor.conf`: Korrektur der JDK- und Icon-Pfads.
|
||||
- `docs/02_Guides/Desktop-Packaging-Guide.md`: Neue Anleitung für Installer-Builds.
|
||||
- `docs/90_Reports/Network-POC-Testplan.md`: Neuer Testplan für die Vernetzung.
|
||||
- `setup-firewall-linux.sh`: Hilfsskript für Netzwerk-Ports.
|
||||
|
||||
## Verification
|
||||
- **Build:** SUCCESSFUL (Gradle 9.5.0 / JDK 25) ✓.
|
||||
- **UI:** Keine Fokus-Warnungen mehr beim Start ✓.
|
||||
- **Packaging:** RPM-Build erfolgreich und lauffähig ✓.
|
||||
- **Netzwerk:** Discovery fehlgeschlagen (Untersuchung morgen) ❌.
|
||||
|
||||
## Nächste Schritte
|
||||
1. Debugging der mDNS-Discovery (mögliche Ursache: Fedora 44 KDE Firewall-Besonderheiten oder IPv6-Konflikte).
|
||||
2. Analyse des Startup-Fehlers des Conveyor `tar.gz` Pakets.
|
||||
3. Wiederaufnahme der physischen Turnier-Hierarchie (Meilenstein 1), sobald die Vernetzung steht.
|
||||
@@ -0,0 +1,47 @@
|
||||
# 🧹 Journal: Build-Performance & Conveyor Installation
|
||||
|
||||
**Datum:** 11. Mai 2026
|
||||
**Agent:** 🏗️ [Lead Architect] & 🧹 [Curator]
|
||||
|
||||
## 📝 Zusammenfassung
|
||||
|
||||
Der Fokus dieser Session lag auf der Optimierung der Gradle-Build-Performance und der Unterstützung des Users beim
|
||||
Wechsel auf einen neuen Entwicklungsrechner (Ubuntu 26.04). Dabei wurde ein Fehler in der GPG-Key-URL von Conveyor
|
||||
behoben.
|
||||
|
||||
## 🚀 Erledigte Aufgaben
|
||||
|
||||
1. **Gradle Performance Boost:**
|
||||
* `enableWasm` in `gradle.properties` wurde standardmäßig auf `false` gesetzt.
|
||||
* Dies deaktiviert die zeitintensive Kompilation von Kotlin/JS und WASM Artefakten (Portal/Wasm-Shell), wenn diese
|
||||
nicht explizit benötigt werden.
|
||||
* Erwartete Zeitersparnis: ca. 60-70% bei Desktop-fokussierten Builds.
|
||||
|
||||
2. **Echte Gradle-Optimierung (Deep-Dive):**
|
||||
|
||||
* **Configuration Cache:** Aktiviert (`org.gradle.configuration-cache=true`). Reduziert die Startzeit des Builds massiv,
|
||||
besonders bei >80 Modulen.
|
||||
* **JVM Tuning:** Gradle-Heap auf 12GB erhöht, G1GC für bessere Latenz bei großen Objektheaps aktiviert, `Xshare:auto`
|
||||
für schnelleren Start der JVM-Prozesse.
|
||||
* **Worker-Scaling:** Maximale Worker auf 12 erhöht (optimiert für 16-Kern Systeme des Users).
|
||||
* **Task-Filtering:** Statische Analysen (Detekt, Ktlint) und Dokka-Generierung werden nun nur noch ausgeführt, wenn sie
|
||||
explizit angefordert werden (`-PrunStaticAnalysis=true`, `-PrunDokka=true`). Dies verhindert unnötige Last während der
|
||||
normalen Entwicklung.
|
||||
|
||||
3. **Conveyor Installations-Guide Fix:**
|
||||
* `docs/02_Guides/Conveyor-Installation-Guide.md` wurde korrigiert.
|
||||
* Der fehlerhafte GPG-Key-Download-Befehl (404 Error) wurde entfernt.
|
||||
* Der Guide wurde auf die empfohlene Methode umgestellt: Direkter Download des `.deb`-Pakets für Ubuntu, welches das
|
||||
Repository automatisch einrichtet.
|
||||
3. **Dokumentations-Update:**
|
||||
* `Desktop-Packaging-Guide.md` um Sektion "Performance-Optimierung" erweitert.
|
||||
* `MASTER_ROADMAP.md` aktualisiert.
|
||||
|
||||
## ⚠️ Offene Punkte / Nächste Schritte
|
||||
|
||||
* **WASM-Builds in CI:** Die CI-Pipeline muss sicherstellen, dass `-PenableWasm=true` gesetzt ist, um das Portal
|
||||
weiterhin zu bauen.
|
||||
* **PoC Verifikation:** Die Verifikation auf physischer Hardware (Ubuntu 26.04) durch den User steht noch aus.
|
||||
|
||||
---
|
||||
*Status: Änderungen erfolgreich angewendet. Verifikation der Performance-Steigerung durch User-Feedback ausstehend.*
|
||||
+17
-23
@@ -1,32 +1,26 @@
|
||||
# ⚡ ACTIVE TASK: Event- & TurnierAnlage-Wizard Migration
|
||||
# ⚡ ACTIVE TASK: Desktop App - Local Network Chat & Host/Client Setup
|
||||
|
||||
**Status:** 🏗️ In Arbeit
|
||||
**SCS:** Event Management / Desktop App
|
||||
**Branch:** `feature/turnier-anlage-wizard`
|
||||
**SCS:** Desktop App / Infrastructure
|
||||
**Branch:** `feature/desktop-network-chat`
|
||||
|
||||
## 🎯 Aktuelles Ziel
|
||||
1. **Event-Wizard Migration:** Migration des Veranstaltungs-Wizards auf den deklarativen Orchestrator (ADR-0025) abgeschlossen. ✓
|
||||
2. **TurnierAnlage:** Implementierung des Wizards zur Anlage von Turnieren, Bewerben und Abteilungen nach ÖTO-Regeln in der Desktop-App.
|
||||
3. **ÖTO-Validierung:** Integration der Abteilungs-Trennungs-Regeln (§ 39) als Warn-Logik im Wizard.
|
||||
1. **Stabile Netzwerk-Kommunikation:** Implementierung einer robusten P2P-Kommunikation mit Reconnection-Logik und Heartbeats.
|
||||
2. **Multi-Node Architektur:** Host-Client-Modell stabilisiert.
|
||||
3. **Professional Packaging:** Vorbereitung für echte Installer (.msi, .deb) via Conveyor.
|
||||
|
||||
## 🛠️ Letzte Änderungen
|
||||
- Event-Wizard: `EventFlowSample.kt` erfolgreich nach `EventWizardFlow.kt` migriert, umbenannt und um ÖTO-Schritte erweitert. ✓
|
||||
- Wissens-Sicherung Plan-B: Caddy & Pangolin Runbook vervollständigt (MIME, COOP/COEP, SMTP-Härtung). ✓
|
||||
- CI/CD: Gitea-Action für automatisierte Docker-Builds bei Git-Tags (`v*`) aktiviert. ✓
|
||||
- TurnierAnlage: `TurnierAnlageFlow.kt` Skelett erstellt. ✓
|
||||
- **Hardening P2P:** `JvmP2pSyncService` komplett refactored. Jetzt mit automatischem Reconnect (3s Intervall) und Ktor Heartbeats (Ping/Pong alle 5s).
|
||||
- **Conveyor:** Konfiguration (`conveyor.conf`) für v1.0.1 vorbereitet (größere JVM Heaps, Linux Abhängigkeiten).
|
||||
- **Firewall Script:** Verbessert und um Kommentare/mDNS erweitert.
|
||||
|
||||
## 📍 Fokus-Dateien
|
||||
- `frontend/features/veranstaltung-feature/src/commonMain/kotlin/at/mocode/veranstaltung/feature/wizard/EventWizardFlow.kt`
|
||||
- `frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/frontend/features/turnier/wizard/TurnierAnlageFlow.kt`
|
||||
- `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
|
||||
- `frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/CreateBewerbWizardScreen.kt`
|
||||
|
||||
## 🚧 Offene Punkte / Blocker
|
||||
- [ ] Erstellung der Compose-Screens für `TurnierBasisdatenStep`.
|
||||
- [ ] Erstellung der Compose-Screens für `TurnierKategorieStep`.
|
||||
- [ ] Implementierung der ÖTO-Check Logik für Abteilungen.
|
||||
- [ ] Sync-Logik zum Backend für die Web-Generierung vorbereiten.
|
||||
## 📍 Fokus-Bereiche
|
||||
- [x] Robuste Reconnection-Logik im P2P Service.
|
||||
- [x] Heartbeats zur Erkennung toter Verbindungen.
|
||||
- [ ] In-App Feedback bei Firewall-Blockaden.
|
||||
- [ ] Multi-Node Test mit > 2 Teilnehmern.
|
||||
|
||||
## 🔄 Nächste Schritte
|
||||
- [ ] Implementierung von `TurnierBasisdatenScreen` (Compose Desktop).
|
||||
- [ ] Verknüpfung des `TurnierAnlageFlow` mit dem UI-Orchestrator.
|
||||
- [ ] Multi-Node Stabilitätstest (Simulierte Netzwerk-Drops).
|
||||
- [ ] Integration von Firewall-Checks im Connectivity-Wizard.
|
||||
- [ ] Erster Test-Build via Conveyor auf lokaler Maschine.
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
# System-Dokumentation: Simka Core Server
|
||||
|
||||
## 1. Wer oder was ist Simka?
|
||||
**Simka** ist einer der beiden zentralen Pfeiler deiner hochperformanten, selbstgehosteten **Keller-Server-Landschaft** (neben dem zweiten Server *Zora*). Während Zora (dein Minisforum MS-R1 ARM64-Arbeitstier) primär für Core-Dienste und Datenhoheit zuständig ist, fungiert Simka als das eigentliche **Performance- und Rechenzentrum** im Keller.
|
||||
|
||||
### Core-Spezifikationen & Hardware-Zustand (Stand: Mai 2026)
|
||||
* **Architektur:** x86_64 High-Performance Mini-PC / Server.
|
||||
* **Betriebssystem / Hypervisor:** **Proxmox VE 9.2** (aktuell auf Kernel/PVE-Stand 9.2.2).
|
||||
* **Kühlung & Wartung:** Frisch saniert im Mai 2026 mit **ARCTIC MX-7** Wärmeleitpaste auf der CPU und **ARCTIC TP-4** High-Performance Wärmeleitpads für optimale thermische Stabilität unter Volllast.
|
||||
* **Speichermedium:** Ultraschnelle NVMe-SSDs für die lokale Container- und VM-Ausführung.
|
||||
|
||||
### Das anstehende "Monster-Upgrade" (Lieferung bis Ende der Woche)
|
||||
Simka wird in den nächsten Tagen hardwareseitig massiv aufgerüstet, um für lokale KI-Workloads und rechenintensive Pipelines bereitzuhoben:
|
||||
1. **Arbeitsspeicher:** Erweiterung auf **64 GB DDR5 4800Mhz RAM** (maximale Kapazität für massives Multitasking und In-Memory-Datenbanken).
|
||||
2. **Grafikkarte / GPU:** **PNY NVIDIA RTX 2000E Ada Generation**
|
||||
* *Formfaktor:* Single-Slot & Low-Profile (perfekt für kompakte Servergehäuse, zieht Strom rein über den PCIe-Steckplatz).
|
||||
* *Leistung:* Extrem energieeffiziente Ada-Lovelace-Architektur mit maximal **50W TDP** im Peak.
|
||||
* *VRAM:* **16 GB Videospeicher mit ECC-Fehlerkorrektur** – das absolute Fundament für den ausfallsicheren 24/7-Betrieb von lokalen LLMs (Large Language Models).
|
||||
|
||||
---
|
||||
|
||||
## 2. Netzwerk-Konfiguration in der Keller-Landschaft
|
||||
|
||||
Simka ist netzwerktechnisch perfekt redundant und isoliert aufgestellt. Die Kommunikation teilt sich auf zwei dedizierte Leitungen auf:
|
||||
|
||||
```
|
||||
[ Erdgeschoss / Internet ]
|
||||
│ (A1-Router / Fritzbox)
|
||||
▼
|
||||
[ Keller-Switch ]
|
||||
│ │
|
||||
│ (10.0.0.20)
|
||||
│ ▼
|
||||
│ ┌───────────┐
|
||||
│ │ ZORA │◄──────┐
|
||||
│ └───────────┘ │
|
||||
│ (10.0.0.40) │ Der exklusive
|
||||
└──────►┌───────────┐ │ Keller-Highway
|
||||
│ SIMKA │ │ (192.168.99.X)
|
||||
└───────────┘ │ Latenz: ~0.3 ms
|
||||
▲ │
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
### A. Der primäre Uplink (`vmbr0`)
|
||||
* **Physisches Interface:** `nic0`
|
||||
* **Lokale IP:** `10.0.0.40/24`
|
||||
* **Gateway:** `10.0.0.138` (A1-Router / Fritzbox)
|
||||
* **Zweck:** Dieser Port verbindet Simka mit dem Hausnetzwerk und dem Internet. Über diesen Pfad greifst du via **Pangolin-Tunnel** (z. B. unter `https://fotos.mo-home.at`) sicher von außen (Remote von der Arbeit) auf deine Dienste zu.
|
||||
|
||||
### B. Der exklusive "Keller-Highway" (`vmbr1`) – *Neu eingerichtet!*
|
||||
* **Physisches Interface:** `nic1` (Direktverbindung per Standard-Netzwerkkabel zu Zoras Interface `enp49s0`).
|
||||
* **Lokale IP:** `192.168.99.2/24`
|
||||
* **Gateway / DNS:** *Keines* (isoliertes Subnetz).
|
||||
* **Status:** Aktiviert mit **Autostart** bei Boot.
|
||||
* **Performance:** Saubere **0,33 ms Latenz** im Ping-Test, 0% Paketverlust.
|
||||
* **Zweck:** Blitzschnelle, vom restlichen Heimnetzwerk komplett isolierte Direktverbindung zwischen Zora und Simka. Hierüber laufen zukünftig automatisierte Backup-Pipelines, CI/CD-Prozesse und der interne Datenaustausch, ohne das normale LAN oder die Router-CPU zu belasten.
|
||||
|
||||
---
|
||||
|
||||
## 3. Container & Dienste: Wer läuft wo?
|
||||
|
||||
Aktuell ist die Bereitstellung auf Simka modular und ressourceneffizient über Proxmox-Strukturen gelöst. Das Herzstück bildet die frisch migrierte Fotosammlung.
|
||||
|
||||
### LXC-Container: `simka-immich`
|
||||
* **Umgebung:** `/opt/immich`
|
||||
* **Konfiguration:** Docker Compose Stack.
|
||||
* **Dienste im Stack:**
|
||||
* `immich_server`: Das Hauptsystem der App (erreichbar lokal unter `http://10.0.0.40:2283`).
|
||||
* `immich_machine_learning`: Zuständig für Objekterkennung und CLIP-Inferenz.
|
||||
* `immich_postgres`: Die relationale Datenbank (PostgreSQL 14 mit `pgvector`/`vectorchord` Erweiterung).
|
||||
* `immich_redis`: In-Memory-Datenstruktur-Store für Caching und Job-Queues.
|
||||
* **Daten-Zustand:**
|
||||
* **Datenbank:** Erfolgreich importiert aus der `immich-db-backup.sql` (**610 MB** an Metadaten, Pfaden und Strukturen). Die alte, leere Auto-Datenbank wurde zuvor sauber via `dropdb` gelöscht.
|
||||
* **Bilderpool:** Satte **~780 GB an Daten (131.578 Fotos/Videos)** liegen direkt lokal auf Simkas schneller NVMe-SSD.
|
||||
* **Aktueller Betriebsmodus:** Reine **CPU-Ausführung** (x86_64). Die Mitternachts-Jobs und Worker-Anzahlen für Video-Transcoding sind CPU-schonend konfiguriert (max. 1–2 Worker), um den Server bis zum Wochenende nicht zu überlasten.
|
||||
* **Zugriff:** Voll funktionsfähig lokal sowie mobil über die Android-App via Pangolin-Tunnel (Plattform-SSO wurde für den reibungslosen API-Sync der App deaktiviert).
|
||||
|
||||
---
|
||||
|
||||
## 4. Die Roadmap für das kommende Wochenende 🚀
|
||||
|
||||
Sobald die Hardware geliefert wurde, wird Simka schrittweise zum KI-Kraftpaket ausgebaut:
|
||||
|
||||
1. **Hardware-Injektion:** Einbau der 64 GB DDR5 RAM und der NVIDIA RTX 2000E Ada.
|
||||
2. **Die "Simka Core AI" VM:** Erstellung einer dedizierten virtuellen Maschine (z. B. Ubuntu/Fedora Server) in Proxmox.
|
||||
* *PCIe-Passthrough:* Die RTX 2000E wird exklusiv direkt in diese KI-VM durchgereicht.
|
||||
* *LLM-Infrastruktur:* Installation von **Ollama**. Dank der 16 GB ECC-VRAM laden wir dort mächtige Coding-Modelle wie `qwen2.5-coder:7b` (oder quantisierte 14b-Versionen) für blitzschnelle Code-Generierung in deiner IntelliJ-IDE auf deinem Ubuntu-Entwicklungs-PC.
|
||||
3. **Immich-Beschleunigung:** Umstellung des `simka-immich` Docker-Stacks auf GPU-Inferenz. Die 88 Tensor-Kerne der RTX übernehmen dann die Gesichtserkennung und das Video-Transcoding in Millisekunden, wodurch die CPU komplett entlastet wird.
|
||||
4. **CI/CD-Ausbau:** Migration des Gitea-Runners für dein Projekt **Meldestelle** aus der alten VM-Infrastruktur in einen schlanken, nativen LXC-Container auf Simka, der über den 0,3-ms-Highway direkt an dein Gitea auf Zora andockt.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. Architektur- und Betriebs-Einschätzung (Ablage)
|
||||
|
||||
🏗️ [Lead Architect]
|
||||
|
||||
### Einschätzung der Architektur
|
||||
- Starker, sinnvoller Plattform-Split: Zora = Core/Control-Plane, Simka = Compute/Throughput. Das entspricht einer sauberen Trennung von „Authority“ und „Acceleration“ und passt zur Desktop-/Offline‑First Strategie der Meldestelle.
|
||||
- Netzwerk-Topologie mit dediziertem „Keller-Highway“ (vmbr1, 192.168.99.0/24) ist genau richtig für Replikation, CI/CD und Bulk-Daten. Minimiert Latenz/Jitter und entkoppelt vom Heimnetz.
|
||||
- Hardware-Entscheidung: RTX 2000E Ada (16 GB ECC, 50 W) ist ein hoch‑effizienter, 24/7‑tauglicher Inferenz‑Beschleuniger. Für LLMs bis 7B (FP16) und 14B (ggf. quantisiert) sowie NVENC/Transcoding ist sie sehr passend. Gegenüber „dickeren“ Karten (z. B. 4090/6000 Ada) verlierst du Peak‑Throughput, gewinnst aber Effizienz, Geräusch, thermische Stabilität und ECC.
|
||||
|
||||
### Architektur-Empfehlungen (kurz & konkret)
|
||||
- Datenpfade klar trennen:
|
||||
- „Hot“ AI/Transcode scratch auf lokaler NVMe (Simka)
|
||||
- „Warm“ Artifacts/Backups via vmbr1 zu Zora
|
||||
- Standards definieren (Doku‑as‑Code):
|
||||
- ADR: „GPU‑Passthrough für Compute‑VMs“ (Begründung, Alternativen, Trade‑offs)
|
||||
- ADR: „LXC vs. VM für Runner/Builds“ (siehe DevOps‑Teil)
|
||||
- Observability als Querschnitt: Einheitliche Metrik‑Namen/Labels für Hosts, VMs, LXC (Node exporter/Nvidia exporter), damit Kapazitätsplanung reproduzierbar ist.
|
||||
|
||||
---
|
||||
|
||||
🐧 [DevOps Engineer]
|
||||
|
||||
### Was ist gut gelaufen
|
||||
- Proxmox VE 9.2 auf aktuellem Stand, dedizierter LXC‑Stack für Immich, Ping‑Latenz ~0,33 ms auf vmbr1: sehr solide Grundlage.
|
||||
- Saubere Migration der Immich‑Datenbank und großer Medienpool lokal auf NVMe → I/O‑Bottlenecks minimiert.
|
||||
|
||||
### Konkreter Wochenend‑Plan (Checkliste)
|
||||
1) Hardware‑Einbau und Firmware
|
||||
- RAM auf 64 GB einsetzen, Memtest (mind. 1 Pass) → Verifikation erforderlich.
|
||||
- GPU einsetzen; BIOS/UEFI:
|
||||
- Primäre Anzeige auf iGPU/Onboard setzen (damit dGPU frei für Passthrough bleibt)
|
||||
- Resizable BAR an (falls angeboten), SR‑IOV/IOMMU aktiv.
|
||||
|
||||
2) Proxmox: NVIDIA GPU‑Passthrough für KI‑VM
|
||||
- Kernel/Boot: IOMMU aktivieren (`amd_iommu=on`), Neustart; prüfen, dass GPU in eigenem IOMMU‑Group landet.
|
||||
- Host: `vfio-pci` binden, Nouveau/NVIDIA‑Hosttreiber nicht laden (Host soll die GPU nicht claimen).
|
||||
- VM anlegen (Ubuntu Server LTS oder Fedora Server), PCIe‑Gerät hinzufügen (GPU + ggf. GPU‑Audio Function), MSI‑Interrupts aktivieren.
|
||||
- In‑Guest: NVIDIA Treiber + CUDA/CuDNN installieren; `nvidia-smi` muss die Karte stabil zeigen.
|
||||
|
||||
3) „Simka Core AI“ VM: Modelle & Runtime
|
||||
- Ollama + optional OpenWebUI installieren.
|
||||
- Modelle (Beispiele mit 16 GB VRAM):
|
||||
- qwen2.5‑coder:7b (FP16) oder 14b quantisiert (Q4_K_M/Q6_K)
|
||||
- DeepSeek‑coder 6.7B, Llama‑3.1/3.2 8B Instruct quantisiert für Tools/Chat
|
||||
- Policies:
|
||||
- Max. Parallelität und KV‑Cache Limits definieren, um OOM zu vermeiden.
|
||||
- Watchdog/Service‑Unit für automatischen Neustart bei Treiber‑Resets.
|
||||
|
||||
4) Immich auf GPU beschleunigen
|
||||
- Auf dem LXC‑Host NVIDIA Container Toolkit bereitstellen oder, falls LXC zu restriktiv ist: Immich in eine leichte VM oder in einen privilegierten LXC migrieren.
|
||||
- docker‑compose Anpassungen:
|
||||
- `immich_machine_learning` mit `--gpus=all`/`runtime: nvidia`
|
||||
- FFmpeg HWAccel aktivieren: `-hwaccel cuda -hwaccel_output_format cuda -c:v h264_nvenc/hevc_nvenc`
|
||||
- Worker‑Limits neu justieren (mehr Transcode‑Jobs, wenn GPU an Bord)
|
||||
- Verifikation: Testtranscode und Embedding‑Job messen (Metriken/Logs sichern).
|
||||
|
||||
5) Gitea Runner Migration (Meldestelle CI/CD)
|
||||
- Entscheidung: LXC vs. VM
|
||||
- LXC (privileged, `nesting=1`, `keyctl=1`) ist ressourcenschonend, aber Docker‑in‑LXC erfordert Sorgfalt.
|
||||
- Alternativ: kleine „Runner‑VM“ mit Docker – oft robuster bei komplexen Build‑Needs (z. B. Android/Compose Desktop Toolchains).
|
||||
- Netzwerk: Runner nutzt vmbr1, verbindet sich mit Gitea auf Zora, DNS via Hostfile/Static Route, kein Default‑GW nötig.
|
||||
- Caching: Maven/Gradle Cache persistent auf separatem Volume; Artefakt‑Upload über vmbr1.
|
||||
|
||||
6) Netzwerk‑Feinschliff
|
||||
- vmbr1 ohne Gateway belassen; auf Hosts `metric`/Policy‑Routing setzen, damit Traffic für 192.168.99.0/24 strikt lokal bleibt.
|
||||
- Optional Jumbo Frames (MTU 9000) testen, wenn NICs/Switch/Direct‑Link es stabil können.
|
||||
- Proxmox‑Firewall:
|
||||
- vmbr0 restriktiv (nur notwendige Ingress‑Ports)
|
||||
- vmbr1 nur Host‑zu‑Host/Backup/CI erlauben
|
||||
|
||||
7) Backup & Wiederherstellung
|
||||
- Immich: DB Dumps (pg_dump) + Objekt‑Speicher Sync via vmbr1 zu Zora (z. B. borg/restic/ZFS send‑recv).
|
||||
- Proxmox: regelmäßige VM/LXC Snapshots + Offhost‑Kopie.
|
||||
- Recovery‑Drills: 1x/Quartal Wiederherstellung in Test‑VM durchführen → „Verifikation ausstehend“ bis Log vorhanden.
|
||||
|
||||
8) Observability & Betrieb
|
||||
- Node Exporter + NVIDIA DCGM/Exporter auf Simka; Prometheus/Grafana auf Zora aggregiert.
|
||||
- Alarme: NVMe Wear‑Level, GPU ECC Errors, Temp, OOM‑Kills, Docker/Container Restarts.
|
||||
- Energie: Optional `nvidia-smi -pl` Power‑Limit dokumentieren (wenn thermisch nötig), persistent via Systemd Unit.
|
||||
|
||||
### Risiken/Watchouts
|
||||
- LXC + GPU ist möglich, aber distributions‑/kernel‑abhängig; wenn Treiber‑Bindung hakelt, früh auf VM umschwenken.
|
||||
- RTX 2000E hat 16 GB VRAM: bei mehreren gleichzeitigen LLM‑Sessions aggressiv quantisieren oder serielle Ausführung planen.
|
||||
- Pangolin‑Tunnel: Secrets/Token sicher hinterlegen und Rotation terminieren; 2FA erzwingen.
|
||||
|
||||
---
|
||||
|
||||
🧹 [Curator]
|
||||
|
||||
### Dokumentations- und Abschluss‑Tasks
|
||||
- Dieses „Simka Core Server“ Dokument ist ein sehr guter Start. Ergänze bitte:
|
||||
- Abschnitt „Verifikation/Beweise“ mit Links/Screenshots/Logs zu: Memtest, `nvidia-smi`, Immich‑GPU‑Transcode‑Probe, Ollama Inferenz‑Benchmark, CI‑Runner „connected“ Status.
|
||||
- ADR‑Einträge unter `docs/01_Architecture/ADRs/`:
|
||||
- ADR‑00X „Dedizierter Keller‑Highway (vmbr1) für Bulk/CI/Backup“
|
||||
- ADR‑00X „GPU‑Passthrough für KI‑VM auf Proxmox“
|
||||
- ADR‑00X „Runner: LXC (privileged) vs. kleine VM – Entscheidung & Gründe“
|
||||
- Runbook „Simka Operations“: Boot‑Reihenfolge, Health‑Checks, Troubleshooting (GPU Reset, Treiber‑Reinstall, Container‑Restart).
|
||||
- Anti‑Halluzinations‑Protokoll anwenden:
|
||||
- Kein „erledigt“ ohne Build/Test‑Beweis; markiere alle neuen Punkte als „Verifikation ausstehend“, bis Logs/Artefakte abgelegt sind.
|
||||
- Inventar & Versionen pflegen:
|
||||
- BIOS/UEFI‑Version, Proxmox‑Kernel, NVIDIA‑Treiber, Docker Compose Hash, Immich Version, DB Schema Version.
|
||||
|
||||
### Verifikation/Beweise (Platzhalter – Verifikation ausstehend)
|
||||
- [ ] Memtest86+ Log (mind. 1 Pass, fehlerfrei)
|
||||
- [ ] `nvidia-smi` Ausgabe in der KI‑VM (GPU erkannt, ECC aktiv, Treiber‑Version)
|
||||
- [ ] Immich: GPU‑beschleunigter Transcode‑Test (Log + Metriken)
|
||||
- [ ] Ollama: Inferenz‑Benchmark (Modell + Prompt + Zeit + VRAM‑Auslastung)
|
||||
- [ ] Gitea Runner: „connected“ Status + Beispiel‑Build‑Log über vmbr1
|
||||
@@ -0,0 +1,72 @@
|
||||
🎨 [Frontend Expert]
|
||||
|
||||
### Kurzfazit
|
||||
- Die beiden Rechner sehen sich über mDNS höchstwahrscheinlich wegen Firewall‑Regeln/Zonen nicht. JmDNS registriert korrekt auf 192.168.0.124:8090, aber der Client (192.168.0.167, WLAN) empfängt keine mDNS‑Antworten → Fedora/Firewalld blockiert UDP 5353 Multicast in der aktiven Zone (WLAN/Public) sehr häufig standardmäßig.
|
||||
- Der Datei‑Picker unter Fedora KDE hakt im Verzeichnis‑Modus vermutlich wegen der Kombination DIRECTORIES_ONLY + SAVE_DIALOG und/oder wegen unserer „schreibbar“-Validierung.
|
||||
|
||||
### Sofortmaßnahmen (auf beiden Geräten, Host + Client)
|
||||
1) Firewalld freischalten (mdns + Ports) und neu laden
|
||||
```
|
||||
sudo firewall-cmd --get-active-zones
|
||||
# notieren, in welcher Zone das aktive Interface liegt (z. B. public, home)
|
||||
ZONE=public # ggf. anpassen
|
||||
|
||||
# mDNS erlauben (UDP 5353 Multicast)
|
||||
sudo firewall-cmd --zone=$ZONE --add-service=mdns --permanent
|
||||
|
||||
# unsere Dienste öffnen
|
||||
sudo firewall-cmd --zone=$ZONE --add-port=8090/tcp --permanent # WS-Chat
|
||||
sudo firewall-cmd --zone=$ZONE --add-port=8080/tcp --permanent # P2P-Server (PoC)
|
||||
|
||||
# Regeln anwenden
|
||||
sudo firewall-cmd --reload
|
||||
|
||||
# Kontrolle
|
||||
sudo firewall-cmd --zone=$ZONE --list-all
|
||||
```
|
||||
Hinweis: WLAN und Ethernet können in unterschiedlichen Zonen liegen (z. B. public vs. home). Bitte in beiden relevanten Zonen freischalten.
|
||||
|
||||
2) Optional: Avahi als Indikator prüfen (nicht zwingend für JmDNS, aber gut für mDNS‑Baseline)
|
||||
```
|
||||
systemctl status avahi-daemon
|
||||
```
|
||||
|
||||
3) mDNS‑Pakete verifizieren (nur wenn es nach 1) noch nicht klappt)
|
||||
```
|
||||
# Auf dem Client (WLAN‑Interface anpassen):
|
||||
sudo tcpdump -n -i wlp0s20f3 port 5353
|
||||
# Auf dem Host (Ethernet‑Interface anpassen):
|
||||
sudo tcpdump -n -i <eth-iface> port 5353
|
||||
```
|
||||
Erwartung: Bei App‑Start siehst du Multicast an 224.0.0.251. Wenn auf einer Seite „Stille“ herrscht, blockiert noch etwas.
|
||||
|
||||
4) WS‑Konnektivität direkt testen (um Networking unabhängig von mDNS zu prüfen)
|
||||
```
|
||||
# vom Client auf den Host
|
||||
websocat ws://192.168.0.124:8090/chat
|
||||
# oder wscat: wscat -c ws://192.168.0.124:8090/chat
|
||||
```
|
||||
Wenn das klappt, ist der WS‑Pfad ok; dann bleibt nur mDNS/Firewall als Ursache.
|
||||
|
||||
### Erklärung zu deinen Logs
|
||||
- „[Discovery] Dienst 'fedora-stefan' … (Port 8090)“ → Host hat korrekt via mDNS veröffentlicht.
|
||||
- „Discovery Update: 0 Dienste gefunden“ am Client → keine mDNS‑Antworten angekommen (typisches Firewall‑Symptom bei Fedora/KDE, vor allem in Zone „public“).
|
||||
- Mehrfach „[P2P Server] Gestartet auf Port 8080“ → Wir haben noch einen Trigger, der den P2P‑Start mehrfach feuert. Funktional unkritisch für den aktuellen Befund; ich bereinige das im nächsten Patch mit einem zusätzlichen Guard.
|
||||
|
||||
### Fedora KDE Datei‑Picker (JFileChooser) – Workarounds jetzt, Fix als Patch im Anschluss
|
||||
- Mögliche Ursache 1: `SAVE_DIALOG` im Ordner‑Modus unter KDE verhält sich zickig. Besser: `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus + eigener Bestätigungs‑Button‑Text.
|
||||
- Mögliche Ursache 2: Unsere Validierung „Ordner existiert und ist beschreibbar“ verhindert die Übernahme (z. B. externer Stick nur lesbar, NTFS ohne Schreibrechte, Netzwerkpfad ohne Rechte). Teste bitte einmal mit einem lokalen Pfad wie `~/MeldestelleBackup`.
|
||||
- Quick‑Test jetzt: Tippe im editierbaren Feld manuell `~/MeldestelleBackup`, bestätige → sollte grün werden. Falls rot: bitte den Text der Fehlermeldung nennen.
|
||||
|
||||
Geplanter Fix (nach deiner Bestätigung):
|
||||
- Directory‑Picker: Wechsel auf `showOpenDialog(null)` mit `JFileChooser.DIRECTORIES_ONLY`, `approveButtonText = "Auswählen"`, `acceptAllFileFilterUsed=false`. Fallback auf HOME, wenn `currentDirectory` ungültig. Beibehalt der freien Texteingabe.
|
||||
- Validierung: Fehlermeldung präziser („existiert nicht“ vs. „keine Schreibrechte“) und angebotene Auto‑Erstellung, wenn der User bestätigt.
|
||||
|
||||
### Bitte liefere kurz zurück
|
||||
- Nach Schritt 1 (firewalld): Finden sich Host (192.168.0.124) und Client (192.168.0.167) gegenseitig? Tauchen Services in der UI/Logs auf?
|
||||
- Falls noch nicht: kurzer Ausschnitt aus `tcpdump` beider Seiten (je 3–5 Zeilen genügen).
|
||||
- Datei‑Picker auf dem Client: Was passiert bei manuellem Pfad `~/MeldestelleBackup`? Erscheint eine Fehlermeldung? Wenn ja, welcher Text?
|
||||
|
||||
### Nächste Schritte (nach Feedback)
|
||||
- Ich liefere: Patch für den KDE‑Picker (OPEN_DIALOG) und einen zusätzlichen Guard gegen mehrfachen P2P‑Start; außerdem noch etwas Discovery‑Logging (Interface/Zonen‑Hinweis).
|
||||
- Danach kümmern wir uns um Conveyor (Windows‑Installer aus CI, inkl. JVM‑Flag gegen die Netty‑Warnung).
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+80
-32
@@ -8,9 +8,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.filechooser.FileNameExtensionFilter
|
||||
|
||||
@Composable
|
||||
actual fun MsFilePicker(
|
||||
@@ -23,17 +26,45 @@ actual fun MsFilePicker(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val currentValue = selectedPath ?: ""
|
||||
val (isError, errorMessage) = run {
|
||||
if (!enabled) false to null
|
||||
else if (currentValue.isBlank()) false to null
|
||||
else {
|
||||
val f = File(currentValue)
|
||||
if (directoryOnly) {
|
||||
when {
|
||||
!f.exists() -> true to "Ordner existiert nicht"
|
||||
!f.isDirectory -> true to "Pfad ist kein Ordner"
|
||||
!f.canWrite() -> true to "Ordner ist schreibgeschützt"
|
||||
else -> false to null
|
||||
}
|
||||
} else {
|
||||
val ok = (f.exists() && f.isFile && f.canWrite()) || (f.parentFile?.canWrite() == true)
|
||||
(!ok) to if (!ok) {
|
||||
when {
|
||||
!f.exists() && f.parentFile?.exists() != true -> "Pfad existiert nicht"
|
||||
f.exists() && !f.isFile -> "Pfad ist keine Datei"
|
||||
else -> "Datei/Ordner nicht beschreibbar"
|
||||
}
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MsTextField(
|
||||
value = selectedPath ?: "",
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
value = currentValue,
|
||||
onValueChange = { newValue -> onFileSelected(newValue) },
|
||||
readOnly = false,
|
||||
label = label,
|
||||
helpDescription = helpDescription,
|
||||
placeholder = if (directoryOnly) "Verzeichnis wählen..." else "Datei wählen...",
|
||||
isError = isError,
|
||||
errorMessage = errorMessage,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = enabled,
|
||||
compact = true
|
||||
@@ -43,39 +74,56 @@ actual fun MsFilePicker(
|
||||
|
||||
MsButton(
|
||||
onClick = {
|
||||
// Einheitlich plattformübergreifend: Swing JFileChooser verwenden
|
||||
SwingUtilities.invokeLater {
|
||||
val chooser = JFileChooser().apply {
|
||||
isMultiSelectionEnabled = false
|
||||
isAcceptAllFileFilterUsed = false
|
||||
approveButtonText = "Auswählen"
|
||||
|
||||
// Initiales Verzeichnis/Pfad
|
||||
run {
|
||||
val home = File(System.getProperty("user.home") ?: ".")
|
||||
val initial = selectedPath?.takeIf { it.isNotBlank() }?.let { File(it) }
|
||||
val baseDir = when {
|
||||
initial == null -> home
|
||||
directoryOnly && initial.isDirectory -> initial
|
||||
!directoryOnly && initial.isFile -> initial.parentFile ?: home
|
||||
initial.parentFile?.isDirectory == true -> initial.parentFile
|
||||
else -> home
|
||||
}
|
||||
currentDirectory = baseDir
|
||||
if (!directoryOnly && initial?.isFile == true) selectedFile = initial
|
||||
}
|
||||
|
||||
if (directoryOnly) {
|
||||
// AWT FileDialog für nativen Look auch bei Verzeichnissen (Windows/Linux/macOS)
|
||||
// unter macOS erzwingt dies die Verzeichnisauswahl. Unter Windows/Linux ist es der Standard-Dialog.
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "true")
|
||||
val dialog = FileDialog(null as Frame?, label, FileDialog.LOAD).apply {
|
||||
selectedPath?.let {
|
||||
val currentDir = File(it)
|
||||
if (currentDir.exists()) {
|
||||
directory = currentDir.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.isVisible = true
|
||||
if (dialog.directory != null && dialog.file != null) {
|
||||
// Bei FileDialog.LOAD unter Windows/Linux wählt man oft eine Datei im Ordner,
|
||||
// aber wir wollen den Ordner. Wir nehmen also das Verzeichnis.
|
||||
onFileSelected(File(dialog.directory, dialog.file).parentFile.absolutePath)
|
||||
} else if (dialog.directory != null) {
|
||||
onFileSelected(dialog.directory)
|
||||
}
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "false")
|
||||
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
// KDE/Plasma: OPEN_DIALOG im DIRECTORIES_ONLY‑Modus verwenden (kein Save‑Dialog)
|
||||
dialogType = JFileChooser.OPEN_DIALOG
|
||||
} else {
|
||||
// AWT FileDialog für nativen Look bei Dateiauswahl (wie vom User gewünscht)
|
||||
val dialog = FileDialog(null as Frame?, label, FileDialog.LOAD).apply {
|
||||
fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
if (fileExtensions.isNotEmpty()) {
|
||||
setFilenameFilter { _, name ->
|
||||
fileExtensions.any { name.lowercase().endsWith(it.lowercase()) }
|
||||
fileFilter = FileNameExtensionFilter(
|
||||
"Erlaubte Dateien",
|
||||
*fileExtensions.map { it.trimStart('.') }.toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.isVisible = true
|
||||
if (dialog.file != null) {
|
||||
onFileSelected(File(dialog.directory, dialog.file).absolutePath)
|
||||
|
||||
val result = chooser.showOpenDialog(null)
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
val chosen = chooser.selectedFile
|
||||
if (directoryOnly) {
|
||||
if (!chosen.exists()) {
|
||||
try {
|
||||
Files.createDirectories(Path.of(chosen.absolutePath))
|
||||
} catch (_: Exception) { /* ignorieren, Validierung zeigt Fehler */ }
|
||||
}
|
||||
onFileSelected(chosen.absolutePath)
|
||||
} else {
|
||||
onFileSelected(chosen.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -60,6 +60,16 @@ sqldelight {
|
||||
create("AppDatabase") {
|
||||
packageName.set("at.mocode.frontend.core.localdb")
|
||||
generateAsync.set(true)
|
||||
// Workaround für SQLite-Temp-Verzeichnis Issue auf Windows
|
||||
verifyMigrations.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround für SQLite-Temp-Verzeichnis Issue auf Windows. Das Plugin generiert dynamisch Tasks.
|
||||
// lazy task configuration avoids cache issues and intercepts dynamic tasks
|
||||
tasks.configureEach {
|
||||
if (name.contains("verify", ignoreCase = true) && name.contains("Migration", ignoreCase = true)) {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
-9
@@ -2,12 +2,6 @@ package at.mocode.frontend.core.localdb
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
* Thin wrapper around SQLDelight `AppDatabase` creation.
|
||||
*
|
||||
* The platform-specific part is the `DatabaseDriverFactory` (expect/actual),
|
||||
* which provides the appropriate SQLDelight driver (JVM sqlite driver, JS WebWorkerDriver, ...).
|
||||
*/
|
||||
class DatabaseProvider(
|
||||
private val driverFactory: DatabaseDriverFactory
|
||||
) {
|
||||
@@ -17,9 +11,6 @@ class DatabaseProvider(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Koin module to provide the SQLDelight database for all frontend targets.
|
||||
*/
|
||||
val localDbModule = module {
|
||||
single<DatabaseDriverFactory> { DatabaseDriverFactory() }
|
||||
single<DatabaseProvider> { DatabaseProvider(get()) }
|
||||
|
||||
@@ -51,5 +51,10 @@ kotlin {
|
||||
implementation(libs.ktor.client.js)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-6
@@ -13,6 +13,11 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Überwacht die Konnektivität zum API-Gateway.
|
||||
*
|
||||
* Robustere Strategie:
|
||||
* 1) /actuator/health/readiness
|
||||
* 2) /actuator/health (Fallback)
|
||||
* 3) /api/ping/simple (Fallback)
|
||||
*/
|
||||
class ConnectivityTracker : KoinComponent {
|
||||
private val client: HttpClient by inject(named("baseHttpClient"))
|
||||
@@ -24,20 +29,47 @@ class ConnectivityTracker : KoinComponent {
|
||||
fun startTracking() {
|
||||
if (scope.isActive && _isOnline.value) return // Bereits aktiv (Dummy-Check)
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
// Sofort prüfen
|
||||
_isOnline.value = checkConnection()
|
||||
// Zweiter Check nach kurzer Wartezeit, um Start-Races zu glätten
|
||||
delay(3_000.milliseconds)
|
||||
_isOnline.value = checkConnection()
|
||||
// Danach im Intervall prüfen
|
||||
while (isActive) {
|
||||
delay(10_000.milliseconds) // Alle 10 Sekunden prüfen
|
||||
_isOnline.value = checkConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkConnection(): Boolean {
|
||||
return try {
|
||||
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/actuator/health/readiness")
|
||||
response.status.value in 200..299
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
val base = NetworkConfig.baseUrl.trimEnd('/')
|
||||
// 1) readiness
|
||||
try {
|
||||
val r1 = client.get("$base/actuator/health/readiness")
|
||||
if (r1.status.value in 200..299) return true
|
||||
} catch (e: Exception) {
|
||||
// Debug-Log schlank halten
|
||||
println("[Connectivity] readiness failed: ${e.message}")
|
||||
}
|
||||
|
||||
// 2) health
|
||||
try {
|
||||
val r2 = client.get("$base/actuator/health")
|
||||
if (r2.status.value in 200..299) return true
|
||||
} catch (e: Exception) {
|
||||
println("[Connectivity] health failed: ${e.message}")
|
||||
}
|
||||
|
||||
// 3) public ping via gateway routing
|
||||
try {
|
||||
val r3 = client.get("$base/api/ping/simple")
|
||||
if (r3.status.value in 200..299) return true
|
||||
} catch (e: Exception) {
|
||||
println("[Connectivity] ping/simple failed: ${e.message}")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun stopTracking() {
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package at.mocode.frontend.core.network
|
||||
|
||||
import at.mocode.frontend.core.network.chat.chatModule
|
||||
import at.mocode.frontend.core.network.discovery.discoveryModule
|
||||
import at.mocode.frontend.core.network.sync.syncModule
|
||||
import io.ktor.client.*
|
||||
@@ -26,7 +27,7 @@ interface TokenProvider {
|
||||
* - "apiClient": Konfigurierter Client für das API-Gateway (Auth-Header, Retry, Timeout)
|
||||
*/
|
||||
val networkModule: Module = module {
|
||||
includes(discoveryModule, syncModule)
|
||||
includes(discoveryModule, syncModule, chatModule)
|
||||
|
||||
single<ConnectivityTracker> { ConnectivityTracker() }
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package at.mocode.frontend.core.network.chat
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Einfaches Chat-Message Modell für lokale Host/Client-Kommunikation.
|
||||
*/
|
||||
@Serializable
|
||||
data class ChatMessage(
|
||||
val sender: String,
|
||||
val message: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package at.mocode.frontend.core.network.chat
|
||||
|
||||
import org.koin.core.module.Module
|
||||
|
||||
/**
|
||||
* Erwartetes Koin-Modul für Chat/WS-Server.
|
||||
*/
|
||||
expect val chatModule: Module
|
||||
+3
-1
@@ -9,6 +9,8 @@ data class DiscoveredService(
|
||||
val name: String,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
/** Optional: expliziter WebSocket-Port, falls vom Haupt-Port abweichend. */
|
||||
val websocketPort: Int? = null,
|
||||
val metadata: Map<String, String> = emptyMap()
|
||||
)
|
||||
|
||||
@@ -36,7 +38,7 @@ interface NetworkDiscoveryService {
|
||||
|
||||
/**
|
||||
* Registriert den eigenen Dienst, damit andere Instanzen ihn finden können.
|
||||
* @param port Der Port, auf dem der lokale WebSocket-Server lauscht.
|
||||
* @param port Der Haupt-Port des Dienstes (z. B. HTTP/API). Der WebSocket-Port wird zusätzlich als Metadatum veröffentlicht.
|
||||
* @param preferredIp Optional eine IP-Adresse, an die der Discovery-Dienst gebunden werden soll.
|
||||
* @param deviceName Der Name des Geräts, das im Netzwerk angezeigt werden soll.
|
||||
*/
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package at.mocode.frontend.core.network.chat
|
||||
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val chatModule: Module = module {
|
||||
// Ktor WebSocket Server (lokaler Host)
|
||||
single { KtorWebSocketServerService() }
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package at.mocode.frontend.core.network.chat
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Einfacher Ktor WebSocket Server für lokale Chat-Kommunikation.
|
||||
*/
|
||||
class KtorWebSocketServerService(
|
||||
private val port: Int = DEFAULT_PORT
|
||||
) {
|
||||
|
||||
private var server: EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>? = null
|
||||
|
||||
private val connections = mutableSetOf<DefaultWebSocketServerSession>()
|
||||
private val lock = Mutex()
|
||||
|
||||
fun start() {
|
||||
if (server != null) return
|
||||
|
||||
val engine = embeddedServer(Netty, port = port, host = "0.0.0.0") {
|
||||
install(WebSockets)
|
||||
install(ContentNegotiation) {
|
||||
json(Json)
|
||||
}
|
||||
|
||||
routing {
|
||||
webSocket("/chat") {
|
||||
// Verbindung merken
|
||||
lock.withLock { connections.add(this) }
|
||||
try {
|
||||
for (frame in incoming) {
|
||||
when (frame) {
|
||||
is Frame.Text -> {
|
||||
val text = frame.readText()
|
||||
// JSON -> ChatMessage
|
||||
val msg = try {
|
||||
Json.decodeFromString(ChatMessage.serializer(), text)
|
||||
} catch (e: Exception) {
|
||||
application.log.warn("[WS] Ungültige Nachricht: ${e.message}")
|
||||
continue
|
||||
}
|
||||
// Broadcast an alle Clients
|
||||
broadcast(msg)
|
||||
}
|
||||
|
||||
is Frame.Binary -> {
|
||||
// Ignorieren oder in Zukunft unterstützen
|
||||
}
|
||||
|
||||
is Frame.Ping, is Frame.Pong, is Frame.Close -> {
|
||||
// nichts
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
// Verbindung wurde geschlossen
|
||||
} catch (e: Exception) {
|
||||
application.log.error("[WS] Fehler in Session: ${e.message}", e)
|
||||
} finally {
|
||||
lock.withLock { connections.remove(this) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine.start(wait = false)
|
||||
server = engine
|
||||
println("[WS] Ktor WebSocket Server gestartet auf Port $port (Pfad: /chat)")
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
server?.stop(gracePeriodMillis = 1000, timeoutMillis = 3000)
|
||||
server = null
|
||||
println("[WS] Ktor WebSocket Server gestoppt")
|
||||
}
|
||||
|
||||
suspend fun broadcast(message: ChatMessage) {
|
||||
val json = Json.encodeToString(ChatMessage.serializer(), message)
|
||||
val snapshot: List<DefaultWebSocketServerSession> = lock.withLock { connections.toList() }
|
||||
snapshot.forEach { session ->
|
||||
try {
|
||||
session.send(Frame.Text(json))
|
||||
} catch (_: Exception) {
|
||||
// Fehler beim Senden ignorieren; Verbindung wird beim nächsten Empfang entfernt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPort(): Int = port
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PORT: Int = 8090
|
||||
}
|
||||
}
|
||||
+45
-4
@@ -19,12 +19,25 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
private val jmdnsInstances = mutableListOf<JmDNS>()
|
||||
private val SERVICE_TYPE = "_meldestelle._tcp.local."
|
||||
private val discoveredServicesMap = ConcurrentHashMap<String, DiscoveredService>()
|
||||
private val registeredSet = ConcurrentHashMap.newKeySet<String>() // key: "${name}@${addr.hostAddress}:$port"
|
||||
|
||||
// Debounce/Guards
|
||||
@Volatile
|
||||
private var lastStartRequestedAt: Long = 0L
|
||||
@Volatile
|
||||
private var lastStartIp: String? = null
|
||||
|
||||
private val _discoveredServices = MutableStateFlow<List<DiscoveredService>>(emptyList())
|
||||
override val discoveredServices: StateFlow<List<DiscoveredService>> = _discoveredServices.asStateFlow()
|
||||
|
||||
override fun startDiscovery(preferredIp: String?) {
|
||||
if (jmdnsInstances.isNotEmpty()) return
|
||||
// Debounce schnelle Folgeaufrufe mit identischer IP
|
||||
val now = System.currentTimeMillis()
|
||||
if (jmdnsInstances.isNotEmpty() && lastStartIp == preferredIp && (now - lastStartRequestedAt) < 500) {
|
||||
return
|
||||
}
|
||||
lastStartRequestedAt = now
|
||||
lastStartIp = preferredIp
|
||||
|
||||
val addresses = getRelevantAddresses(preferredIp)
|
||||
if (addresses.isEmpty()) {
|
||||
@@ -51,11 +64,14 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
|
||||
override fun serviceResolved(event: ServiceEvent) {
|
||||
val info = event.info
|
||||
val md = info.propertyNames.asSequence().associateWith { info.getPropertyString(it) }
|
||||
val wsPort = md["websocketPort"]?.toIntOrNull()
|
||||
val service = DiscoveredService(
|
||||
name = event.name,
|
||||
host = info.inetAddresses.firstOrNull()?.hostAddress ?: "unknown",
|
||||
port = info.port,
|
||||
metadata = info.propertyNames.asSequence().associateWith { info.getPropertyString(it) }
|
||||
websocketPort = wsPort,
|
||||
metadata = md
|
||||
)
|
||||
discoveredServicesMap[event.name] = service
|
||||
_discoveredServices.value = discoveredServicesMap.values.toList()
|
||||
@@ -103,12 +119,19 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
mapOf(
|
||||
"version" to "1.0.0",
|
||||
"type" to "master",
|
||||
"nodeId" to name
|
||||
"nodeId" to name,
|
||||
// Der Ktor WebSocket-Server lauscht (derzeit) auf demselben Port; kann abweichen
|
||||
"websocketPort" to port.toString()
|
||||
)
|
||||
)
|
||||
try {
|
||||
val key = "${name}@${jmdns.inetAddress.hostAddress}:$port"
|
||||
if (registeredSet.add(key)) {
|
||||
jmdns.registerService(serviceInfo)
|
||||
println("[Discovery] Dienst '$name' auf ${jmdns.inetAddress} registriert (Port $port)")
|
||||
} else {
|
||||
// bereits registriert – kein Spam
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[Discovery] Fehler bei Registrierung auf ${jmdns.inetAddress}: ${e.message}")
|
||||
}
|
||||
@@ -125,13 +148,22 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val iface = interfaces.nextElement()
|
||||
val name = iface.name.lowercase()
|
||||
// Filtere Docker/Bridged/VETH/VM-Schnittstellen heraus
|
||||
if (iface.isLoopback || !iface.isUp || iface.isVirtual) continue
|
||||
if (name.startsWith("br-") || name.startsWith("docker") || name.startsWith("veth") || name.contains("vmnet") || name.contains(
|
||||
"virbr"
|
||||
)
|
||||
) continue
|
||||
|
||||
val inetAddresses = iface.inetAddresses
|
||||
while (inetAddresses.hasMoreElements()) {
|
||||
val addr = inetAddresses.nextElement()
|
||||
// Nur IPv4 für maximale Kompatibilität in lokalen Netzen (ÖTO/FEI Standardumgebungen)
|
||||
if (addr is java.net.Inet4Address) {
|
||||
// Exkludiere Link-Local
|
||||
val host = addr.hostAddress
|
||||
if (host.startsWith("169.254.")) continue
|
||||
addresses.add(addr)
|
||||
}
|
||||
}
|
||||
@@ -140,7 +172,16 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
||||
println("[Discovery] Fehler beim Auflisten der Interfaces: ${e.message}")
|
||||
}
|
||||
|
||||
return if (addresses.isEmpty()) listOf(InetAddress.getLocalHost()) else addresses
|
||||
if (addresses.isEmpty()) return listOf(InetAddress.getLocalHost())
|
||||
|
||||
// Bevorzuge private LAN IPv4 (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||
fun isPrivateIPv4(a: InetAddress): Boolean {
|
||||
val h = a.hostAddress
|
||||
return h.startsWith("192.168.") || h.startsWith("10.") || (h.startsWith("172.") && h.split('.').getOrNull(1)
|
||||
?.toIntOrNull() in 16..31)
|
||||
}
|
||||
return addresses.sortedWith(compareByDescending<InetAddress> { isPrivateIPv4(it) }
|
||||
.thenBy { it.hostAddress })
|
||||
}
|
||||
|
||||
override fun getDiscoveredServices(): List<DiscoveredService> {
|
||||
|
||||
+94
-27
@@ -2,27 +2,41 @@ package at.mocode.frontend.core.network.sync
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* JVM-spezifische Implementierung des P2pSyncService mit Fokus auf Stabilität.
|
||||
* Beinhaltet Reconnection-Logik, Heartbeats und robustes Session-Management.
|
||||
*/
|
||||
class JvmP2pSyncService : P2pSyncService {
|
||||
private var server: EmbeddedServer<*, *>? = null
|
||||
private val client = HttpClient {
|
||||
install(io.ktor.client.plugins.websocket.WebSockets)
|
||||
companion object {
|
||||
private val startedPorts: MutableSet<Int> = ConcurrentHashMap.newKeySet()
|
||||
private const val RECONNECT_DELAY_MS = 3000L
|
||||
private const val PING_INTERVAL_MS = 5000L
|
||||
private const val PING_TIMEOUT_MS = 10000L
|
||||
}
|
||||
|
||||
private val _incomingEvents = MutableSharedFlow<SyncEvent>()
|
||||
private var server: EmbeddedServer<*, *>? = null
|
||||
private var currentPort: Int? = null
|
||||
private val client = HttpClient {
|
||||
install(WebSockets) {
|
||||
pingInterval = PING_INTERVAL_MS.milliseconds
|
||||
}
|
||||
}
|
||||
|
||||
private val _incomingEvents = MutableSharedFlow<SyncEvent>(extraBufferCapacity = 64)
|
||||
override val incomingEvents: Flow<SyncEvent> = _incomingEvents.asSharedFlow()
|
||||
|
||||
private val activeSessions = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
|
||||
@@ -30,15 +44,29 @@ class JvmP2pSyncService : P2pSyncService {
|
||||
override val connectedPeers: Flow<List<String>> = _connectedPeers.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val connectionJobs = ConcurrentHashMap<String, Job>()
|
||||
|
||||
override fun startServer(port: Int) {
|
||||
if (server != null) return
|
||||
if (server != null) {
|
||||
println("[P2P Server] Bereits aktiv auf Port ${currentPort ?: port}")
|
||||
return
|
||||
}
|
||||
|
||||
server = embeddedServer(Netty, port = port) {
|
||||
install(io.ktor.server.websocket.WebSockets)
|
||||
if (!startedPorts.add(port)) {
|
||||
println("[P2P Server] Port $port wird bereits von einer anderen Instanz genutzt.")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
server = embeddedServer(Netty, port = port, host = "0.0.0.0") {
|
||||
install(io.ktor.server.websocket.WebSockets) {
|
||||
pingPeriod = PING_INTERVAL_MS.milliseconds
|
||||
timeout = PING_TIMEOUT_MS.milliseconds
|
||||
}
|
||||
routing {
|
||||
webSocket("/sync") {
|
||||
println("[P2P Server] Neuer Peer verbunden")
|
||||
val remote = call.request.local.remoteAddress
|
||||
println("[P2P Server] Neuer Peer verbunden: $remote")
|
||||
activeSessions.add(this)
|
||||
updatePeers()
|
||||
try {
|
||||
@@ -48,32 +76,56 @@ class JvmP2pSyncService : P2pSyncService {
|
||||
try {
|
||||
val event = Json.decodeFromString<SyncEvent>(text)
|
||||
_incomingEvents.emit(event)
|
||||
} catch (e: Exception) {
|
||||
println("[P2P Server] Fehler beim Dekodieren: ${e.message}")
|
||||
} catch (ex: Exception) {
|
||||
println("[P2P Server] Fehler beim Dekodieren von $remote: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
println("[P2P Server] Verbindung zu $remote unterbrochen: ${ex.message}")
|
||||
} finally {
|
||||
activeSessions.remove(this)
|
||||
updatePeers()
|
||||
println("[P2P Server] Peer getrennt")
|
||||
println("[P2P Server] Peer $remote getrennt")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start(wait = false)
|
||||
println("[P2P Server] Gestartet auf Port $port")
|
||||
currentPort = port
|
||||
println("[P2P Server] Erfolgreich gestartet auf Port $port")
|
||||
} catch (ex: Exception) {
|
||||
startedPorts.remove(port)
|
||||
server = null
|
||||
currentPort = null
|
||||
println("[P2P Server] Fehler beim Starten des Servers auf Port $port: ${ex.message}")
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopServer() {
|
||||
connectionJobs.values.forEach { it.cancel() }
|
||||
connectionJobs.clear()
|
||||
try {
|
||||
server?.stop(1000, 2000)
|
||||
} finally {
|
||||
server = null
|
||||
currentPort?.let { startedPorts.remove(it) }
|
||||
currentPort = null
|
||||
println("[P2P Server] Server gestoppt.")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun connectToPeer(host: String, port: Int) {
|
||||
scope.launch {
|
||||
val peerKey = "$host:$port"
|
||||
|
||||
connectionJobs[peerKey]?.cancel()
|
||||
|
||||
val job = scope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
println("[P2P Client] Verbindungsversuch zu $peerKey...")
|
||||
client.webSocket(host = host, port = port, path = "/sync") {
|
||||
println("[P2P Client] Verbunden mit $host:$port")
|
||||
println("[P2P Client] Verbunden mit $peerKey")
|
||||
activeSessions.add(this)
|
||||
updatePeers()
|
||||
try {
|
||||
@@ -84,32 +136,47 @@ class JvmP2pSyncService : P2pSyncService {
|
||||
_incomingEvents.emit(event)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
println("[P2P Client] Verbindung zu $peerKey abgebrochen: ${ex.message}")
|
||||
} finally {
|
||||
activeSessions.remove(this)
|
||||
updatePeers()
|
||||
println("[P2P Client] Verbindung zu $host:$port beendet")
|
||||
println("[P2P Client] Session mit $peerKey beendet.")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[P2P Client] Fehler bei Verbindung zu $host:$port: ${e.message}")
|
||||
} catch (ex: Exception) {
|
||||
println("[P2P Client] Konnte keine Verbindung zu $peerKey herstellen: ${ex.message}")
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
println("[P2P Client] Erneuter Versuch für $peerKey in ${RECONNECT_DELAY_MS}ms...")
|
||||
delay(RECONNECT_DELAY_MS.milliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
connectionJobs[peerKey] = job
|
||||
}
|
||||
|
||||
override suspend fun broadcastEvent(event: SyncEvent) {
|
||||
val text = Json.encodeToString(event)
|
||||
activeSessions.toList().forEach { session ->
|
||||
val sessions = activeSessions.toList()
|
||||
sessions.forEach { session ->
|
||||
try {
|
||||
if (session.isActive) {
|
||||
session.send(Frame.Text(text))
|
||||
} catch (e: Exception) {
|
||||
println("[P2P] Fehler beim Senden an Session: ${e.message}")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Session wird durch Heartbeat/Loop automatisch bereinigt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePeers() {
|
||||
// Da wir keine einfachen IPs in den Sessions haben ohne tieferes Casting,
|
||||
// nutzen wir hier erst mal einen Platzhalter oder zählen nur.
|
||||
_connectedPeers.value = activeSessions.map { "Peer-${it.hashCode()}" }
|
||||
_connectedPeers.value = activeSessions.map { session ->
|
||||
when (session) {
|
||||
is DefaultWebSocketServerSession -> session.call.request.local.remoteAddress
|
||||
else -> "Outgoing-Peer"
|
||||
}
|
||||
}.distinct()
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package at.mocode.frontend.core.network.sync
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
|
||||
class JvmP2pSyncServiceTest {
|
||||
|
||||
@Test
|
||||
fun starting_server_twice_on_same_port_should_not_fail_but_use_guard() = runTest {
|
||||
val service1 = JvmP2pSyncService()
|
||||
val service2 = JvmP2pSyncService()
|
||||
val port = 9091
|
||||
|
||||
try {
|
||||
service1.startServer(port)
|
||||
// Second start should just return/log and not throw an exception (idempotent)
|
||||
service2.startServer(port)
|
||||
} finally {
|
||||
service1.stopServer()
|
||||
service2.stopServer()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stopping_server_should_release_port_lock() = runTest {
|
||||
val service1 = JvmP2pSyncService()
|
||||
val service2 = JvmP2pSyncService()
|
||||
val port = 9092
|
||||
|
||||
service1.startServer(port)
|
||||
service1.stopServer()
|
||||
|
||||
// After stopping, starting again on same port (even from different instance) should work
|
||||
service2.startServer(port)
|
||||
service2.stopServer()
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package at.mocode.frontend.core.network.chat
|
||||
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
// Auf WASM/JS gibt es keinen lokalen Ktor-Server; bereiten ein leeres Modul vor.
|
||||
actual val chatModule: Module = module { }
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package at.mocode.frontend.core.network.sync
|
||||
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
* Wasm-spezifische Implementierung (vorerst No-op).
|
||||
|
||||
+3
-1
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.features.device.initialization.domain.DeviceInitializationValidator
|
||||
import at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting
|
||||
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
private fun DiscoveryRadar(
|
||||
@@ -94,7 +96,7 @@ fun DeviceInitializationScreen(
|
||||
// Automatische Discovery starten
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.startDiscovery()
|
||||
roleSelectorFocus.requestFocus()
|
||||
delay(100.milliseconds); withFrameMillis { roleSelectorFocus.requestFocus() }
|
||||
}
|
||||
|
||||
Surface(
|
||||
|
||||
+3
-2
@@ -208,10 +208,11 @@ class DeviceInitializationViewModel(
|
||||
discoveryService.stopDiscovery()
|
||||
discoveryService.startDiscovery(ip)
|
||||
|
||||
// Falls wir ein Master sind, registrieren wir uns auch direkt, damit andere uns finden
|
||||
// Falls wir ein Master sind, starten wir den lokalen P2P‑Server.
|
||||
// Die mDNS‑Registrierung erfolgt zentral beim App‑Start (entkoppelt, um Duplikate zu vermeiden).
|
||||
if (uiState.value.settings.networkRole == NetworkRole.MASTER) {
|
||||
discoveryService.registerService(8080, ip, uiState.value.settings.deviceName)
|
||||
syncService.startServer(8080)
|
||||
println("[P2P Server] Gestartet auf Port 8080")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -45,4 +45,23 @@ actual object DeviceInitializationSettingsManager {
|
||||
val settings = loadSettings() ?: return false
|
||||
return DeviceInitializationValidator.canContinue(settings)
|
||||
}
|
||||
|
||||
// Hilfsfunktionen (nur JVM): Pfad anzeigen und Reset durchführen
|
||||
fun getSettingsFilePath(): String = settingsFile.absolutePath
|
||||
|
||||
/**
|
||||
* Setzt die Desktop-App lokal zurück.
|
||||
* - Löscht settings.json (Device-Initialization)
|
||||
* - Optional: Löscht die lokale Datenbank unter ~/.meldestelle
|
||||
*/
|
||||
fun resetToFactoryDefaults(deleteDatabase: Boolean = false): Result<Unit> = try {
|
||||
if (settingsFile.exists()) settingsFile.delete()
|
||||
if (deleteDatabase) {
|
||||
val dbDir = File(System.getProperty("user.home"), ".meldestelle")
|
||||
if (dbDir.exists()) dbDir.deleteRecursively()
|
||||
}
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
+78
-28
@@ -48,12 +48,6 @@ actual fun DeviceInitializationConfig(
|
||||
val focusManager = LocalFocusManager.current
|
||||
val (_, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (settings.deviceName.isEmpty()) {
|
||||
deviceNameFocus.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
@@ -66,7 +60,7 @@ actual fun DeviceInitializationConfig(
|
||||
value = settings.deviceName,
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } },
|
||||
label = "Gerätename",
|
||||
helpDescription = "Ein eindeutiger Name für diesen PC (z.B. 'Richter-Springplatz').",
|
||||
helpDescription = "Ein eindeutiger Name für diesen PC (z.B. Richter-Springplatz).",
|
||||
placeholder = "z.B. Meldestelle-PC-1",
|
||||
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
||||
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
||||
@@ -77,24 +71,43 @@ actual fun DeviceInitializationConfig(
|
||||
compact = true
|
||||
)
|
||||
|
||||
// NETZWERK-INTERFACES (EXPERTEN-MODUS)
|
||||
val interfaces = remember {
|
||||
NetworkInterface.getNetworkInterfaces().toList()
|
||||
.filter { it.isUp && !it.isLoopback && it.inetAddresses.hasMoreElements() }
|
||||
.filter { it.isUp && !it.isLoopback && it.inetAddresses.hasMoreElements() && !it.name.startsWith("br-") && !it.name.startsWith("docker") && !it.name.startsWith("veth") }
|
||||
.map { ni ->
|
||||
val friendlyName = when {
|
||||
ni.displayName.contains("wlan", ignoreCase = true) || ni.displayName.contains("wi-fi", ignoreCase = true) || ni.name.contains("wlan", ignoreCase = true) -> "🌐 WLAN"
|
||||
ni.displayName.contains("eth", ignoreCase = true) || ni.displayName.contains("ethernet", ignoreCase = true) || ni.name.contains("eth", ignoreCase = true) || ni.name.contains("en", ignoreCase = true) -> "🔌 Ethernet"
|
||||
ni.displayName.contains("wlan", ignoreCase = true) || ni.displayName.contains(
|
||||
"wi-fi",
|
||||
ignoreCase = true
|
||||
) || ni.name.contains("wlan", ignoreCase = true) -> "🌐 WLAN"
|
||||
|
||||
ni.displayName.contains("eth", ignoreCase = true) || ni.displayName.contains(
|
||||
"ethernet",
|
||||
ignoreCase = true
|
||||
) || ni.name.contains("eth", ignoreCase = true) || ni.name.contains(
|
||||
"en",
|
||||
ignoreCase = true
|
||||
) -> "🔌 Ethernet"
|
||||
|
||||
else -> "💻 " + ni.displayName
|
||||
}
|
||||
val address = ni.inetAddresses.asSequence().firstOrNull { !it.isLinkLocalAddress && it.hostAddress.indexOf(':') == -1 }?.hostAddress
|
||||
val address = ni.inetAddresses.asSequence()
|
||||
.firstOrNull { !it.isLinkLocalAddress && it.hostAddress.indexOf(":") == -1 }?.hostAddress
|
||||
?: ni.inetAddresses.nextElement().hostAddress
|
||||
|
||||
val isConnected = !ni.isLoopback && ni.isUp && ni.interfaceAddresses.any {
|
||||
it.address.isSiteLocalAddress || it.address.hostAddress.startsWith("192.168") || it.address.hostAddress.startsWith("10.")
|
||||
it.address.isSiteLocalAddress || it.address.hostAddress.startsWith("192.168") || it.address.hostAddress.startsWith(
|
||||
"10."
|
||||
)
|
||||
}
|
||||
|
||||
InterfaceInfo(id = "$friendlyName ($address)", name = friendlyName, address = address, hardwareName = ni.name, isConnected = isConnected)
|
||||
InterfaceInfo(
|
||||
id = "$friendlyName ($address)",
|
||||
name = friendlyName,
|
||||
address = address,
|
||||
hardwareName = ni.name,
|
||||
isConnected = isConnected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,12 +133,17 @@ actual fun DeviceInitializationConfig(
|
||||
Surface(
|
||||
onClick = { if (!uiState.isLocked) viewModel.updateSettings { s -> s.copy(networkInterface = info.id) } },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(
|
||||
alpha = 0.3f
|
||||
),
|
||||
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(10.dp).background(if (info.isConnected) Color(0xFF4CAF50) else Color(0xFFF44336), CircleShape))
|
||||
Box(
|
||||
Modifier.size(10.dp)
|
||||
.background(if (info.isConnected) Color(0xFF4CAF50) else Color(0xFFF44336), CircleShape)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(info.name, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
|
||||
@@ -137,13 +155,12 @@ actual fun DeviceInitializationConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// SICHERHEITSSCHLÜSSEL
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
MsTextField(
|
||||
value = settings.sharedKey,
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } },
|
||||
label = "Sicherheitsschlüssel (Sync-Key)",
|
||||
helpDescription = "Das 'Turnier-Passwort'. Muss auf allen Geräten gleich sein.",
|
||||
helpDescription = "Das Turnier-Passwort. Muss auf allen Geräten gleich sein.",
|
||||
placeholder = "Mindestens 8 Zeichen",
|
||||
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
|
||||
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
|
||||
@@ -157,7 +174,6 @@ actual fun DeviceInitializationConfig(
|
||||
compact = true
|
||||
)
|
||||
|
||||
// CLIENT-VERBINDUNG-FEEDBACK
|
||||
if (settings.networkRole == NetworkRole.CLIENT && !uiState.isLocked) {
|
||||
val masterSelected = uiState.selectedMaster != null
|
||||
val canConnect = masterSelected && settings.sharedKey.isNotBlank()
|
||||
@@ -170,13 +186,19 @@ actual fun DeviceInitializationConfig(
|
||||
else -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f)
|
||||
},
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
border = BorderStroke(1.dp, when (uiState.connectionStatus) {
|
||||
border = BorderStroke(
|
||||
1.dp, when (uiState.connectionStatus) {
|
||||
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50)
|
||||
ConnectionStatus.FAILED -> Color(0xFFF44336)
|
||||
else -> MaterialTheme.colorScheme.outlineVariant
|
||||
})
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Column(Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
when (uiState.connectionStatus) {
|
||||
ConnectionStatus.CONNECTING -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
@@ -210,11 +232,25 @@ actual fun DeviceInitializationConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// BACKUP & DRUCKER
|
||||
MsFilePicker(
|
||||
label = "Backup-Verzeichnis (Plan-USB)",
|
||||
selectedPath = settings.backupPath,
|
||||
onFileSelected = { viewModel.updateSettings { s -> s.copy(backupPath = it) } },
|
||||
onFileSelected = { path ->
|
||||
if (path.isNotBlank()) {
|
||||
try {
|
||||
val dir = java.io.File(path)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val probe = java.io.File(dir, ".ms_write_test.tmp")
|
||||
probe.writeText("ok")
|
||||
probe.delete()
|
||||
viewModel.updateSettings { s -> s.copy(backupPath = path) }
|
||||
} catch (e: Exception) {
|
||||
println("[DeviceInit] Backup-Verzeichnis nicht beschreibbar: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
viewModel.updateSettings { s -> s.copy(backupPath = path) }
|
||||
}
|
||||
},
|
||||
directoryOnly = true,
|
||||
modifier = Modifier.focusRequester(backupPathFocus),
|
||||
enabled = !uiState.isLocked
|
||||
@@ -231,10 +267,13 @@ actual fun DeviceInitializationConfig(
|
||||
)
|
||||
}
|
||||
|
||||
// MASTER: ERWARTETE CLIENTS
|
||||
if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) {
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
||||
TextButton(onClick = { viewModel.addExpectedClient() }) {
|
||||
Icon(Icons.Default.Add, null, Modifier.size(18.dp))
|
||||
@@ -280,7 +319,12 @@ actual fun DeviceInitializationConfig(
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = { viewModel.removeExpectedClient(index) }) {
|
||||
Icon(Icons.Default.Delete, null, tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(20.dp))
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
|
||||
@@ -292,4 +336,10 @@ actual fun DeviceInitializationConfig(
|
||||
}
|
||||
}
|
||||
|
||||
private data class InterfaceInfo(val id: String, val name: String, val address: String, val hardwareName: String, val isConnected: Boolean)
|
||||
private data class InterfaceInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val address: String,
|
||||
val hardwareName: String,
|
||||
val isConnected: Boolean
|
||||
)
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
/**
|
||||
* Dieses Modul kapselt die gesamte UI und Logik für das Ping-Feature.
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
@@ -17,14 +14,9 @@ version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
wasmJs {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
browser { testTask { enabled = false } }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -33,12 +25,13 @@ kotlin {
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.sync)
|
||||
implementation(projects.frontend.core.localDb)
|
||||
implementation(projects.frontend.core.auth) // Added auth module for AuthTokenManager
|
||||
implementation(projects.frontend.core.auth)
|
||||
implementation(libs.sqldelight.coroutines)
|
||||
implementation(projects.frontend.core.domain)
|
||||
|
||||
implementation(compose.foundation)
|
||||
// Explizite Compose-Abhängigkeiten zur Vermeidung von Gradle 10 Warnungen
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
@@ -49,7 +42,7 @@ kotlin {
|
||||
implementation(libs.bundles.compose.common)
|
||||
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose) // Added koin.compose for koinInject
|
||||
implementation(libs.koin.compose)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
@@ -72,6 +65,5 @@ kotlin {
|
||||
wasmJsMain.dependencies {
|
||||
implementation(libs.kotlin.stdlib.wasm.js)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Shell-Modul: Meldestelle Desktop App
|
||||
* Reines JVM/Compose-Desktop-Modul – Desktop-First gemäß MASTER_ROADMAP.
|
||||
* Setzt alle Core- und Feature-Module zu einer lauffähigen Desktop-Anwendung zusammen.
|
||||
*
|
||||
* Packaging:
|
||||
* ./gradlew :frontend:shells:meldestelle-desktop:packageDeb → Linux .deb
|
||||
* ./gradlew :frontend:shells:meldestelle-desktop:packageMsi → Windows .msi
|
||||
* ./gradlew :frontend:shells:meldestelle-desktop:packageDmg → macOS .dmg
|
||||
* ./gradlew :frontend:shells:meldestelle-desktop:packageReleaseDistributables → alle Plattformen
|
||||
*
|
||||
* Version: Wird automatisch aus version.properties im Root-Projekt gelesen (SemVer).
|
||||
* Icons: src/jvmMain/resources/icon.png / icon.ico / icon.icns
|
||||
* → siehe ICONS_PLACEHOLDER.md für Anforderungen
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
@@ -26,16 +11,12 @@ plugins {
|
||||
group = "at.mocode.frontend.shell"
|
||||
version = "1.0.0"
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Version aus root version.properties lesen (SemVer)
|
||||
// ---------------------------------------------------------------
|
||||
val versionProps = Properties().also { props ->
|
||||
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
||||
}
|
||||
val vMajor: String? = versionProps.getProperty("VERSION_MAJOR", "1")
|
||||
val vMinor: String? = versionProps.getProperty("VERSION_MINOR", "0")
|
||||
val vPatch: String? = versionProps.getProperty("VERSION_PATCH", "0")
|
||||
// nativeDistributions erwartet reines "MAJOR.MINOR.PATCH" (kein Qualifier)
|
||||
val packageVer = "$vMajor.$vMinor.$vPatch"
|
||||
|
||||
kotlin {
|
||||
@@ -43,7 +24,6 @@ kotlin {
|
||||
|
||||
sourceSets {
|
||||
jvmMain.dependencies {
|
||||
// Core-Module
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
@@ -54,10 +34,8 @@ kotlin {
|
||||
implementation(projects.frontend.core.auth)
|
||||
implementation(projects.core.znsParser)
|
||||
|
||||
// Feature-Module
|
||||
implementation(projects.frontend.features.pingFeature)
|
||||
implementation(projects.frontend.features.nennungFeature)
|
||||
|
||||
implementation(projects.frontend.features.znsImportFeature)
|
||||
implementation(projects.frontend.features.veranstalterFeature)
|
||||
implementation(projects.frontend.features.veranstaltungFeature)
|
||||
@@ -70,7 +48,6 @@ kotlin {
|
||||
implementation(projects.frontend.features.billingFeature)
|
||||
implementation(projects.frontend.features.deviceInitialization)
|
||||
|
||||
// Compose Desktop
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
@@ -80,17 +57,13 @@ kotlin {
|
||||
implementation(compose.uiTooling)
|
||||
implementation(libs.composeHotReloadApi)
|
||||
|
||||
// DI (Koin)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
|
||||
// Bundles
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.bundles.compose.common)
|
||||
implementation(libs.logback.classic)
|
||||
}
|
||||
|
||||
jvmTest.dependencies {
|
||||
@@ -104,12 +77,8 @@ compose.desktop {
|
||||
mainClass = "at.mocode.frontend.shell.desktop.MainKt"
|
||||
|
||||
nativeDistributions {
|
||||
// Ziel-Formate: Linux .deb, Windows .msi, macOS .dmg
|
||||
targetFormats(TargetFormat.Deb, TargetFormat.Msi, TargetFormat.Dmg)
|
||||
targetFormats(TargetFormat.Deb, TargetFormat.Rpm, TargetFormat.Msi, TargetFormat.Dmg)
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Gemeinsame App-Metadaten
|
||||
// -------------------------------------------------------
|
||||
packageName = "meldestelle"
|
||||
packageVersion = packageVer
|
||||
description = "ÖTO-konforme Turnier-Meldestelle – Desktop App"
|
||||
@@ -117,53 +86,30 @@ compose.desktop {
|
||||
copyright = "© 2024–2026 mo-code.at. Alle Rechte vorbehalten."
|
||||
licenseFile.set(rootProject.file("LICENSE"))
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Linux (.deb)
|
||||
// -------------------------------------------------------
|
||||
linux {
|
||||
// PNG 512×512 px — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
|
||||
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
||||
packageName = "meldestelle"
|
||||
// Debian-Kategorie
|
||||
appCategory = "misc"
|
||||
// Menü-Eintrag
|
||||
menuGroup = "Meldestelle"
|
||||
shortcut = true
|
||||
debMaintainer = "support@mo-code.at"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Windows (.msi)
|
||||
// -------------------------------------------------------
|
||||
windows {
|
||||
// ICO Multi-Size — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
|
||||
iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
|
||||
// Eindeutige GUID für Windows Installer Upgrade-Erkennung
|
||||
// WICHTIG: Diese UUID darf sich NIE ändern!
|
||||
upgradeUuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
menuGroup = "Meldestelle"
|
||||
// Startmenü-Verknüpfung
|
||||
shortcut = true
|
||||
// Desktop-Verknüpfung
|
||||
dirChooser = true
|
||||
perUserInstall = false
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// macOS (.dmg)
|
||||
// -------------------------------------------------------
|
||||
macOS {
|
||||
// ICNS 1024×1024 px — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
|
||||
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
||||
bundleID = "at.mocode.meldestelle"
|
||||
appCategory = "public.app-category.productivity"
|
||||
// Für notarisierten Release: signing-Konfiguration hier ergänzen
|
||||
// signing { sign.set(true); identity.set("Developer ID Application: ...") }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// JVM-Laufzeit-Konfiguration (eingebettetes JRE)
|
||||
// -------------------------------------------------------
|
||||
modules(
|
||||
"java.base",
|
||||
"java.desktop",
|
||||
@@ -176,8 +122,8 @@ compose.desktop {
|
||||
)
|
||||
}
|
||||
|
||||
// JVM-Argumente für die gepackte Anwendung
|
||||
jvmArgs(
|
||||
"--enable-native-access=ALL-UNNAMED",
|
||||
"-Xms128m",
|
||||
"-Xmx512m",
|
||||
"-Dfile.encoding=UTF-8",
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ import org.koin.compose.viewmodel.koinViewModel
|
||||
*/
|
||||
@Composable
|
||||
fun DesktopApp() {
|
||||
val deviceInitViewModel: at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel = koinViewModel()
|
||||
val deviceInitViewModel: at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel =
|
||||
koinViewModel()
|
||||
val deviceSettings by deviceInitViewModel.uiState.collectAsState()
|
||||
|
||||
val isDark = when (deviceSettings.settings.appTheme) {
|
||||
|
||||
-2
@@ -35,8 +35,6 @@ private fun PreviewContent() {
|
||||
// --- VEREIN ---
|
||||
|
||||
|
||||
|
||||
|
||||
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
||||
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
||||
// VeranstalterNeuScreen(onBack = {}, onSave = {})
|
||||
|
||||
+6
@@ -6,8 +6,12 @@ import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
||||
import at.mocode.frontend.core.navigation.CurrentUserProvider
|
||||
import at.mocode.frontend.core.navigation.DeepLinkHandler
|
||||
import at.mocode.frontend.core.navigation.NavigationPort
|
||||
import at.mocode.frontend.shell.desktop.data.repository.StoreVeranstaltungRepository
|
||||
import at.mocode.frontend.shell.desktop.navigation.DesktopNavigationPort
|
||||
import at.mocode.frontend.shell.desktop.repository.DesktopMasterdataRepository
|
||||
import at.mocode.frontend.shell.desktop.screens.chat.presentation.ChatViewModel
|
||||
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
@@ -35,4 +39,6 @@ val desktopModule = module {
|
||||
single<CurrentUserProvider> { DesktopCurrentUserProvider(get()) }
|
||||
single { DeepLinkHandler(get(), get()) }
|
||||
single<MasterdataRepository> { DesktopMasterdataRepository(get()) }
|
||||
single<VeranstaltungRepository> { StoreVeranstaltungRepository() }
|
||||
viewModel { ChatViewModel(get()) }
|
||||
}
|
||||
|
||||
+31
-45
@@ -1,14 +1,12 @@
|
||||
package at.mocode.frontend.shell.desktop
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.application
|
||||
import at.mocode.frontend.core.auth.di.authModule
|
||||
import at.mocode.frontend.core.localdb.AppDatabase
|
||||
import at.mocode.frontend.core.localdb.DatabaseProvider
|
||||
import at.mocode.frontend.core.localdb.localDbModule
|
||||
import at.mocode.frontend.core.network.networkModule
|
||||
import at.mocode.frontend.core.network.sync.SyncManager
|
||||
import at.mocode.frontend.core.sync.di.syncModule
|
||||
import at.mocode.frontend.features.billing.di.billingModule
|
||||
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
|
||||
@@ -22,64 +20,52 @@ import at.mocode.frontend.features.turnier.di.turnierFeatureModule
|
||||
import at.mocode.frontend.features.veranstalter.di.veranstalterModule
|
||||
import at.mocode.frontend.features.verein.di.vereinFeatureModule
|
||||
import at.mocode.frontend.features.zns.import.di.znsImportModule
|
||||
import at.mocode.frontend.shell.desktop.data.repository.StoreVeranstaltungRepository
|
||||
import at.mocode.frontend.shell.desktop.di.desktopModule
|
||||
import at.mocode.veranstaltung.feature.di.veranstaltungModule
|
||||
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.context.loadKoinModules
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.dsl.module
|
||||
|
||||
fun main() = application {
|
||||
try {
|
||||
startKoin {
|
||||
fun main() {
|
||||
application {
|
||||
// Koin Starten
|
||||
val koinApp = startKoin {
|
||||
printLogger()
|
||||
modules(
|
||||
networkModule,
|
||||
syncModule,
|
||||
authModule,
|
||||
localDbModule,
|
||||
pingFeatureModule,
|
||||
nennungFeatureModule,
|
||||
znsImportModule,
|
||||
profileModule,
|
||||
billingModule,
|
||||
pferdeModule,
|
||||
reiterModule,
|
||||
funktionaerModule,
|
||||
vereinFeatureModule,
|
||||
veranstalterModule,
|
||||
turnierFeatureModule,
|
||||
veranstaltungModule,
|
||||
module {
|
||||
single<VeranstaltungRepository> { StoreVeranstaltungRepository() }
|
||||
},
|
||||
deviceInitializationModule,
|
||||
desktopModule,
|
||||
deviceInitializationModule,
|
||||
billingModule,
|
||||
funktionaerModule,
|
||||
nennungFeatureModule,
|
||||
pferdeModule,
|
||||
pingFeatureModule,
|
||||
profileModule,
|
||||
reiterModule,
|
||||
turnierFeatureModule,
|
||||
veranstalterModule,
|
||||
veranstaltungModule,
|
||||
vereinFeatureModule,
|
||||
znsImportModule
|
||||
)
|
||||
}
|
||||
println("[DesktopApp] KOIN initialisiert")
|
||||
// Testdaten für Prototyp laden
|
||||
at.mocode.frontend.shell.desktop.data.Store.seed()
|
||||
} catch (e: Exception) {
|
||||
println("[DesktopApp] Koin-Warnung: ${e.message}")
|
||||
}
|
||||
|
||||
try {
|
||||
val provider = GlobalContext.get().get<DatabaseProvider>()
|
||||
val db = runBlocking { provider.createDatabase() }
|
||||
loadKoinModules(module { single<AppDatabase> { db } })
|
||||
println("[DesktopApp] Lokale DB bereit")
|
||||
} catch (e: Exception) {
|
||||
println("[DesktopApp] DB-Warnung: ${e.message}")
|
||||
}
|
||||
val koin = koinApp.koin
|
||||
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Meldestelle",
|
||||
state = WindowState(width = 1600.dp, height = 900.dp),
|
||||
) {
|
||||
// Datenbank initialisieren und als Singleton registrieren
|
||||
val dbProvider: DatabaseProvider = koin.get()
|
||||
val database = runBlocking { dbProvider.createDatabase() }
|
||||
koin.loadModules(listOf(module { single { database } }))
|
||||
|
||||
// SyncManager initialisieren und starten (Default Port 8080)
|
||||
val syncManager: SyncManager = koin.get()
|
||||
syncManager.start(8080)
|
||||
|
||||
Window(onCloseRequest = ::exitApplication, title = "Meldestelle Desktop") {
|
||||
DesktopApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -24,14 +24,14 @@ class DesktopNavigationPort : NavigationPort {
|
||||
}
|
||||
|
||||
override fun navigateToScreen(screen: AppScreen) {
|
||||
println("[DesktopNav] navigateToScreen -> $screen")
|
||||
// Aktuellen Screen auf den Stack legen, falls er nicht derselbe ist
|
||||
val current = _currentScreen.value
|
||||
if (current != screen) {
|
||||
backStack.add(current)
|
||||
// Begrenzung des Backstacks auf z. B. 50 Einträge
|
||||
if (backStack.size > 50) backStack.removeAt(0)
|
||||
if (current == screen) {
|
||||
// Keine Aktion/kein Log bei identischem Ziel – beruhigt die Navigation
|
||||
return
|
||||
}
|
||||
println("[DesktopNav] navigateToScreen -> $screen")
|
||||
backStack.add(current)
|
||||
if (backStack.size > 50) backStack.removeAt(0)
|
||||
_currentScreen.value = screen
|
||||
}
|
||||
|
||||
|
||||
+20
-31
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
@@ -16,30 +17,24 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
data class ChatMessage(
|
||||
val id: String,
|
||||
val sender: String,
|
||||
val text: String,
|
||||
val time: String,
|
||||
val isFromMe: Boolean
|
||||
)
|
||||
import at.mocode.frontend.shell.desktop.screens.chat.presentation.ChatMessageState
|
||||
import at.mocode.frontend.shell.desktop.screens.chat.presentation.ChatViewModel
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun ChatScreen(
|
||||
onBack: () -> Unit
|
||||
onBack: () -> Unit,
|
||||
viewModel: ChatViewModel = koinViewModel()
|
||||
) {
|
||||
var messageText by remember { mutableStateOf("") }
|
||||
val messages = remember { mutableStateListOf<ChatMessage>() }
|
||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||
val messages by viewModel.messages.collectAsState()
|
||||
val peerCount by viewModel.peerCount.collectAsState()
|
||||
val scrollState = rememberLazyListState()
|
||||
|
||||
// Mock initial messages
|
||||
LaunchedEffect(Unit) {
|
||||
if (messages.isEmpty()) {
|
||||
messages.add(ChatMessage("1", "Richter-Turm 1", "Startliste für Bewerb 5 ist fertig?", "10:45", false))
|
||||
messages.add(ChatMessage("2", "Meldestelle", "Ja, wird gerade gedruckt.", "10:46", true))
|
||||
// Auto-scroll to bottom on new messages
|
||||
LaunchedEffect(messages.size) {
|
||||
if (messages.isNotEmpty()) {
|
||||
scrollState.animateScrollToItem(messages.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +56,9 @@ fun ChatScreen(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"LAN-Kanal: aktiv (3 Teilnehmer)",
|
||||
"LAN-Kanal: aktiv ($peerCount Teilnehmer verbunden)",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = AppColors.Success
|
||||
color = if (peerCount > 0) AppColors.Success else MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -71,11 +66,12 @@ fun ChatScreen(
|
||||
|
||||
// Chat Messages
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = Dimens.SpacingM),
|
||||
contentPadding = PaddingValues(vertical = Dimens.SpacingM),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||
) {
|
||||
items(messages) { msg ->
|
||||
items(messages, key = { it.id }) { msg ->
|
||||
ChatBubble(msg)
|
||||
}
|
||||
}
|
||||
@@ -102,18 +98,11 @@ fun ChatScreen(
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (messageText.isNotBlank()) {
|
||||
messages.add(
|
||||
ChatMessage(
|
||||
id = messages.size.toString(),
|
||||
sender = "Meldestelle",
|
||||
text = messageText,
|
||||
time = LocalTime.now().format(timeFormatter),
|
||||
isFromMe = true
|
||||
)
|
||||
)
|
||||
viewModel.sendMessage(messageText)
|
||||
messageText = ""
|
||||
}
|
||||
},
|
||||
enabled = messageText.isNotBlank(),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
@@ -128,7 +117,7 @@ fun ChatScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatBubble(msg: ChatMessage) {
|
||||
private fun ChatBubble(msg: ChatMessageState) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = if (msg.isFromMe) Alignment.End else Alignment.Start
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package at.mocode.frontend.shell.desktop.screens.chat.presentation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.core.network.sync.ChatMessageEvent
|
||||
import at.mocode.frontend.core.network.sync.SyncManager
|
||||
import at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import kotlin.time.Clock
|
||||
|
||||
data class ChatMessageState(
|
||||
val id: String,
|
||||
val sender: String,
|
||||
val text: String,
|
||||
val time: String,
|
||||
val isFromMe: Boolean
|
||||
)
|
||||
|
||||
class ChatViewModel(
|
||||
private val syncManager: SyncManager
|
||||
) : ViewModel() {
|
||||
private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||
private val settings = DeviceInitializationSettingsManager.loadSettings()
|
||||
private val myName = settings?.deviceName ?: "Meldestelle"
|
||||
|
||||
private val _messages = MutableStateFlow<List<ChatMessageState>>(emptyList())
|
||||
val messages: StateFlow<List<ChatMessageState>> = _messages.asStateFlow()
|
||||
|
||||
private val _peerCount = MutableStateFlow(0)
|
||||
val peerCount: StateFlow<Int> = _peerCount.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
syncManager.getIncomingEvents().collect { event ->
|
||||
if (event is ChatMessageEvent) {
|
||||
_messages.update {
|
||||
it + ChatMessageState(
|
||||
id = event.eventId,
|
||||
sender = event.senderName,
|
||||
text = event.message,
|
||||
time = LocalTime.now().format(timeFormatter),
|
||||
isFromMe = event.originNodeId == myName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
syncManager.getConnectedPeers().collect { peers ->
|
||||
_peerCount.value = peers.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(text: String) {
|
||||
if (text.isBlank()) return
|
||||
|
||||
val event = ChatMessageEvent(
|
||||
eventId = UUID.randomUUID().toString(),
|
||||
sequenceNumber = 0,
|
||||
originNodeId = myName,
|
||||
createdAt = Clock.System.now().toEpochMilliseconds(),
|
||||
senderName = myName,
|
||||
message = text
|
||||
)
|
||||
|
||||
// Sofort lokal anzeigen
|
||||
_messages.update {
|
||||
it + ChatMessageState(
|
||||
id = event.eventId,
|
||||
sender = myName,
|
||||
text = text,
|
||||
time = LocalTime.now().format(timeFormatter),
|
||||
isFromMe = true
|
||||
)
|
||||
}
|
||||
|
||||
syncManager.broadcastEvent(event)
|
||||
}
|
||||
}
|
||||
+86
-2
@@ -11,8 +11,7 @@ import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -20,6 +19,10 @@ import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import at.mocode.frontend.core.navigation.AppScreen
|
||||
import at.mocode.frontend.core.network.backup.BackupService
|
||||
import at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
fun DesktopTopHeader(
|
||||
@@ -126,6 +129,87 @@ fun DesktopTopHeader(
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
// Diagnose/Tools: Backup jetzt erstellen + Reset-Aktionen
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
Button(onClick = { menuOpen = true }, enabled = true) {
|
||||
Text("Tools")
|
||||
}
|
||||
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Backup jetzt erstellen (PoC)") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
val settings = DeviceInitializationSettingsManager.loadSettings()
|
||||
val backupPath = settings?.backupPath.orEmpty()
|
||||
val sharedKey = settings?.sharedKey.orEmpty()
|
||||
val deviceName = settings?.deviceName.orEmpty().ifBlank { "Meldestelle-Device" }
|
||||
if (backupPath.isBlank() || sharedKey.isBlank()) {
|
||||
println("[Backup] Abbruch: backupPath oder sharedKey nicht gesetzt. Öffne DeviceInitialization.")
|
||||
onNavigate(AppScreen.DeviceInitialization)
|
||||
} else {
|
||||
try {
|
||||
val backupService: BackupService = GlobalContext.get().get<BackupService> { parametersOf(deviceName) }
|
||||
val result = backupService.exportDelta("poc-backup", backupPath, sharedKey)
|
||||
result.onSuccess { fileName -> println("[Backup] Erfolgreich exportiert: $fileName") }
|
||||
.onFailure { ex -> println("[Backup] Fehler: ${ex.message}") }
|
||||
} catch (e: Exception) {
|
||||
println("[Backup] Fehler bei der Initialisierung: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Einstellungen-Ordner öffnen") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
val settingsDir = DeviceInitializationSettingsManager.getSettingsFilePath()
|
||||
val parent = java.io.File(settingsDir).parentFile?.absolutePath ?: settingsDir
|
||||
try {
|
||||
// Versuche plattformspezifisch den Ordner zu öffnen
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
if (os.contains("win")) {
|
||||
Runtime.getRuntime().exec(arrayOf("explorer", parent))
|
||||
} else if (os.contains("mac")) {
|
||||
Runtime.getRuntime().exec(arrayOf("open", parent))
|
||||
} else {
|
||||
Runtime.getRuntime().exec(arrayOf("xdg-open", parent))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[Tools] Konnte Ordner nicht öffnen: ${e.message}. Pfad: $parent")
|
||||
}
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Einstellungen zurücksetzen") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
val res = DeviceInitializationSettingsManager.resetToFactoryDefaults(deleteDatabase = false)
|
||||
if (res.isSuccess) {
|
||||
println("[Reset] settings.json gelöscht: ${DeviceInitializationSettingsManager.getSettingsFilePath()}")
|
||||
} else {
|
||||
println("[Reset] Fehler: ${res.exceptionOrNull()?.message}")
|
||||
}
|
||||
onNavigate(AppScreen.DeviceInitialization)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Alles zurücksetzen (inkl. DB)") },
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
val res = DeviceInitializationSettingsManager.resetToFactoryDefaults(deleteDatabase = true)
|
||||
if (res.isSuccess) {
|
||||
println("[Reset] settings + ~/.meldestelle gelöscht")
|
||||
} else {
|
||||
println("[Reset] Fehler: ${res.exceptionOrNull()?.message}")
|
||||
}
|
||||
onNavigate(AppScreen.DeviceInitialization)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Profil / Logout Bereich
|
||||
if (isAuthenticated) {
|
||||
Text(
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
<!-- JmDNS ist extrem gesprächig auf DEBUG/TRACE -->
|
||||
<logger name="javax.jmdns" level="INFO"/>
|
||||
<logger name="io.netty" level="INFO"/>
|
||||
</configuration>
|
||||
+16
-29
@@ -4,31 +4,28 @@ android.nonTransitiveRClass=true
|
||||
|
||||
# Kotlin Configuration
|
||||
kotlin.code.style=official
|
||||
# Increased Kotlin Daemon Heap for JS Compilation
|
||||
kotlin.daemon.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g
|
||||
# Increased Kotlin Daemon Heap for JS Compilation + JDK 25 Warning Suppression
|
||||
kotlin.daemon.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --enable-native-access=ALL-UNNAMED
|
||||
kotlin.js.compiler.sourcemaps=false
|
||||
|
||||
# Kotlin Compiler Optimizations (Phase 5)
|
||||
# Kotlin Compiler Optimizations
|
||||
kotlin.incremental=true
|
||||
kotlin.incremental.multiplatform=true
|
||||
kotlin.incremental.js=true
|
||||
|
||||
kotlin.caching.enabled=true
|
||||
kotlin.compiler.execution.strategy=in-process
|
||||
# kotlin.compiler.preciseCompilationResultsBackup=true
|
||||
kotlin.stdlib.default.dependency=true
|
||||
|
||||
# Gradle Configuration
|
||||
# Increased Gradle Daemon Heap
|
||||
org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx6g" -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError -Xshare:off -Djava.awt.headless=true
|
||||
org.gradle.workers.max=8
|
||||
# Optimized for JDK 25: Added --add-opens and --enable-native-access for compiler tools
|
||||
org.gradle.jvmargs=-Xmx12g -Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Xshare:auto -Djava.awt.headless=true --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --enable-native-access=ALL-UNNAMED -Djdk.instrument.traceUsage=false
|
||||
org.gradle.workers.max=12
|
||||
org.gradle.vfs.watch=true
|
||||
|
||||
# Configuration Cache optimieren - TEMPORÄR DEAKTIVIERT wegen JS-Test Serialisierungsproblemen
|
||||
org.gradle.configuration-cache=false
|
||||
# Configuration Cache (Enabled for performance with many modules)
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.configuration-cache.problems=warn
|
||||
|
||||
# Build Performance verbessern
|
||||
# Build Performance
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
@@ -46,7 +43,7 @@ org.jetbrains.kotlin.wasm.check.wasm.binary.format=false
|
||||
kotlin.native.ignoreDisabledTargets=true
|
||||
idea.project.settings.delegate.build.run.actions.to.gradle=true
|
||||
|
||||
# Enable NPM/Yarn lifecycle scripts for Kotlin/JS (required for sql.js & worker setup)
|
||||
# NPM/Yarn lifecycle
|
||||
kotlin.js.yarn.ignoreScripts=false
|
||||
org.jetbrains.kotlin.js.yarn.ignoreScripts=false
|
||||
kotlin.js.npm.ignoreScripts=false
|
||||
@@ -56,31 +53,21 @@ org.jetbrains.kotlin.js.npm.ignoreScripts=false
|
||||
org.gradle.logging.level=lifecycle
|
||||
kotlin.build.report.single_file=false
|
||||
|
||||
# Compose Experimental Features
|
||||
# Compose Experimental
|
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||
org.jetbrains.compose.experimental.wasm.enabled=true
|
||||
|
||||
# Java Toolchain: ensure Gradle auto-downloads a full JDK when needed
|
||||
# Java Toolchain
|
||||
org.gradle.java.installations.auto-download=true
|
||||
org.gradle.java.installations.auto-detect=true
|
||||
|
||||
# Development Environment Support
|
||||
dev.port.offset=0
|
||||
# Set dev.port.offset=100 for second developer
|
||||
# Set dev.port.offset=200 for the third developer
|
||||
# ------------------------------------------------------------------
|
||||
# Wasm/JS Feature Toggle
|
||||
# ------------------------------------------------------------------
|
||||
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
|
||||
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
|
||||
# Feature Toggles
|
||||
enableWasm=true
|
||||
enableDesktop=true
|
||||
dev.port.offset=0
|
||||
|
||||
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
|
||||
# See https://kotl.in/dokka-gradle-migration
|
||||
# org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers
|
||||
# Dokka V2
|
||||
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
|
||||
|
||||
# Workaround for Gradle 9 / KMP "Plugin loaded multiple times" error in Docker/CI
|
||||
# This allows subprojects to re-declare plugins even if they are already on the classpath
|
||||
# Gradle 9 Workaround
|
||||
kotlin.mpp.allowMultiplePluginDeclarations=true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user