Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1690da3fab | |||
| cb6e0103e7 | |||
| 98d0bf0c7b | |||
| 0a90b57c2a | |||
| 0ab62a2752 | |||
| 6070709bf2 | |||
| 763c2a9157 | |||
| 4f715d10bb | |||
| 0b830eb675 | |||
| 4c37ecb952 | |||
| c25ef17a4a | |||
| e5e3b4cfec | |||
| 7d064853e5 | |||
| 387180c12c | |||
| 49393d3eac | |||
| e389fe9bce | |||
| 1a4753cd73 | |||
| ece3f8bf78 | |||
| 8d176ce955 |
@@ -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
|
TODO*.md
|
||||||
NOTES*.md
|
NOTES*.md
|
||||||
**/.junie/
|
.junie/
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Keep essential files (override exclusions)
|
# Keep essential files (override exclusions)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
desktop-tests:
|
desktop-tests:
|
||||||
# Komplett deaktivierbar über Repo-Variable: Settings → Variables → DESKTOP_CI_ENABLED=true
|
# 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
|
# 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
|
name: Compose Desktop — Tests (headless) & Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Build and Publish Docker Images
|
name: Build and Publish Docker Images
|
||||||
run-name: Build & Publish by @${{ github.actor }}
|
run-name: Build & Publish by @${{ gitea.actor }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -117,8 +117,8 @@ jobs:
|
|||||||
images: ${{ env.REGISTRY_INTERNAL }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
|
images: ${{ env.REGISTRY_INTERNAL }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=latest,enable=${{ gitea.ref == 'refs/heads/main' }}
|
||||||
type=sha,format=long,enable=${{ github.ref == 'refs/heads/main' }}
|
type=sha,format=long,enable=${{ gitea.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -132,5 +132,5 @@ jobs:
|
|||||||
provenance: false
|
provenance: false
|
||||||
sbom: false
|
sbom: false
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILD_DATE=${{ github.event.head_commit.timestamp || 'unknown' }}
|
BUILD_DATE=${{ gitea.event.head_commit.timestamp || 'unknown' }}
|
||||||
VERSION=${{ github.sha }}
|
VERSION=${{ gitea.sha }}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
chmod +x install-conveyor.sh
|
chmod +x install-conveyor.sh
|
||||||
./install-conveyor.sh
|
./install-conveyor.sh
|
||||||
fi
|
fi
|
||||||
echo "$HOME/.conveyor/bin" >> $GITHUB_PATH
|
echo "$HOME/.conveyor/bin" >> $GITEA_PATH
|
||||||
|
|
||||||
- name: Windows .msi mit Conveyor bauen
|
- name: Windows .msi mit Conveyor bauen
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
no-hardcoded-versions:
|
no-hardcoded-versions:
|
||||||
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
|
# 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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
tag-release:
|
tag-release:
|
||||||
name: 🏷️ Git-Tag setzen
|
name: 🏷️ Git-Tag setzen
|
||||||
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
|
# 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
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.read-version.outputs.version }}
|
version: ${{ steps.read-version.outputs.version }}
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Git-Tag erstellen & pushen
|
- name: Git-Tag erstellen & pushen
|
||||||
if: steps.check-tag.outputs.already_tagged == 'false' && github.event.inputs.dry_run != 'true'
|
if: steps.check-tag.outputs.already_tagged == 'false' && gitea.event.inputs.dry_run != 'true'
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.read-version.outputs.tag }}"
|
TAG="${{ steps.read-version.outputs.tag }}"
|
||||||
VERSION="${{ steps.read-version.outputs.version }}"
|
VERSION="${{ steps.read-version.outputs.version }}"
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
package-linux:
|
package-linux:
|
||||||
name: 📦 Linux .deb Packaging
|
name: 📦 Linux .deb Packaging
|
||||||
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit
|
# 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
|
runs-on: ubuntu-latest
|
||||||
needs: tag-release
|
needs: tag-release
|
||||||
|
|
||||||
@@ -88,11 +88,11 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup JDK 21 (Temurin)
|
- name: Setup JDK 25 (Temurin)
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
|
|
||||||
- name: Gradle cache
|
- name: Gradle cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -128,7 +128,7 @@ jobs:
|
|||||||
package-windows:
|
package-windows:
|
||||||
name: 📦 Windows .msi Packaging
|
name: 📦 Windows .msi Packaging
|
||||||
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit
|
# 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
|
runs-on: windows-latest
|
||||||
needs: tag-release
|
needs: tag-release
|
||||||
|
|
||||||
@@ -136,11 +136,11 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup JDK 21 (Temurin)
|
- name: Setup JDK 25 (Temurin)
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
|
|
||||||
- name: Gradle cache
|
- name: Gradle cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -179,11 +179,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Summary ausgeben
|
- name: Summary ausgeben
|
||||||
run: |
|
run: |
|
||||||
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITEA_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITEA_STEP_SUMMARY
|
||||||
echo "| Artefakt | Status |" >> $GITHUB_STEP_SUMMARY
|
echo "| Artefakt | Status |" >> $GITEA_STEP_SUMMARY
|
||||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
echo "|----------|--------|" >> $GITEA_STEP_SUMMARY
|
||||||
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITHUB_STEP_SUMMARY
|
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITEA_STEP_SUMMARY
|
||||||
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITHUB_STEP_SUMMARY
|
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITEA_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITEA_STEP_SUMMARY
|
||||||
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITEA_STEP_SUMMARY
|
||||||
|
|||||||
@@ -56,3 +56,7 @@ desktop.ini
|
|||||||
docs/temp/
|
docs/temp/
|
||||||
docs/Bin/
|
docs/Bin/
|
||||||
docs/_archive/
|
docs/_archive/
|
||||||
|
|
||||||
|
# Conveyor
|
||||||
|
conveyor.rootkey
|
||||||
|
output/
|
||||||
|
|||||||
@@ -1,43 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# check-docs-drift.sh
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur.
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||||
# - Kein Guidelines-System mehr.
|
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
# - Single Source of Truth: `docs/`
|
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
|
||||||
|
|
||||||
err=0
|
|
||||||
|
|
||||||
has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; }
|
|
||||||
miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; }
|
|
||||||
|
|
||||||
# Harte Altlast-Pfade dürfen nicht mehr vorkommen
|
|
||||||
if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then
|
|
||||||
echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden"
|
|
||||||
err=1
|
|
||||||
fi
|
|
||||||
if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then
|
|
||||||
echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden"
|
|
||||||
err=1
|
|
||||||
fi
|
|
||||||
if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then
|
|
||||||
echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden"
|
|
||||||
err=1
|
|
||||||
fi
|
|
||||||
if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then
|
|
||||||
echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden"
|
|
||||||
err=1
|
|
||||||
fi
|
|
||||||
if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then
|
|
||||||
echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden"
|
|
||||||
err=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein)
|
|
||||||
has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway"
|
|
||||||
has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway"
|
|
||||||
miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor"
|
|
||||||
has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway"
|
|
||||||
miss docs/01_Architecture/c4/02-container-de.puml "Ktor"
|
|
||||||
|
|
||||||
exit $err
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p build/diagrams
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
shopt -s nullglob
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||||
for f in docs/architecture/c4/*.puml; do
|
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams"
|
exec "$ROOT_DIR/.ai/scripts/render-plantuml.sh" "$@"
|
||||||
echo "Rendered build/diagrams/$(basename "${f%.puml}").svg"
|
|
||||||
done
|
|
||||||
|
|||||||
@@ -1,136 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# validate-links.sh - Link-Validierung für Projektdokumentation (`docs/**`).
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
# Zweck: Guardrail für die "Docs-as-Code"-Strategie.
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||||
|
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
||||||
cd "$PROJECT_ROOT"
|
|
||||||
|
|
||||||
QUICK_MODE=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--quick)
|
|
||||||
QUICK_MODE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help|-h)
|
|
||||||
cat << 'EOF'
|
|
||||||
Docs Link-Validierung
|
|
||||||
|
|
||||||
USAGE:
|
|
||||||
./.junie/scripts/validate-links.sh [--quick]
|
|
||||||
|
|
||||||
BESCHREIBUNG:
|
|
||||||
Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade.
|
|
||||||
Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...).
|
|
||||||
|
|
||||||
OPTIONEN:
|
|
||||||
--quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert).
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "[ERROR] Unbekannter Parameter: $1" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
python3 - <<'PY'
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
root = Path.cwd()
|
|
||||||
docs_dir = root / "docs"
|
|
||||||
|
|
||||||
if not docs_dir.is_dir():
|
|
||||||
print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr)
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
# Veraltete Pfad-Prüfungen wurden entfernt, da sie zu wartungsintensiv waren.
|
|
||||||
# Das Skript konzentriert sich nun auf die Validierung der Link-Integrität.
|
|
||||||
FORBIDDEN_SUBSTRINGS = []
|
|
||||||
|
|
||||||
md_files = sorted(docs_dir.rglob("*.md"))
|
|
||||||
|
|
||||||
link_pattern = re.compile(r"\]\(([^)]+)\)")
|
|
||||||
|
|
||||||
errors = 0
|
|
||||||
|
|
||||||
def is_external(target: str) -> bool:
|
|
||||||
t = target.lower()
|
|
||||||
return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:")
|
|
||||||
|
|
||||||
def strip_fragment_and_query(target: str) -> str:
|
|
||||||
# remove fragment and query parts
|
|
||||||
target = target.split("#", 1)[0]
|
|
||||||
target = target.split("?", 1)[0]
|
|
||||||
return target
|
|
||||||
|
|
||||||
for f in md_files:
|
|
||||||
text = f.read_text(encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
for forbidden in FORBIDDEN_SUBSTRINGS:
|
|
||||||
if forbidden in text:
|
|
||||||
print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}")
|
|
||||||
errors += 1
|
|
||||||
|
|
||||||
for match in link_pattern.finditer(text):
|
|
||||||
target = match.group(1).strip()
|
|
||||||
|
|
||||||
if not target:
|
|
||||||
continue
|
|
||||||
if is_external(target):
|
|
||||||
continue
|
|
||||||
if target.startswith("#"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# drop angle brackets <...> used in markdown for urls with spaces
|
|
||||||
if target.startswith("<") and target.endswith(">"):
|
|
||||||
target = target[1:-1]
|
|
||||||
|
|
||||||
target = unquote(strip_fragment_and_query(target))
|
|
||||||
|
|
||||||
# ignore absolute paths in the repo (we treat them as doc-style links; validate only if relative)
|
|
||||||
if target.startswith("/"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ignore non-file targets (e.g. empty or protocol-less anchors)
|
|
||||||
if ":" in target.split("/", 1)[0]:
|
|
||||||
# things like "vscode:..." etc.
|
|
||||||
continue
|
|
||||||
|
|
||||||
# treat as file path relative to markdown file
|
|
||||||
resolved = (f.parent / target).resolve()
|
|
||||||
|
|
||||||
# keep validation within repo
|
|
||||||
try:
|
|
||||||
resolved.relative_to(root.resolve())
|
|
||||||
except ValueError:
|
|
||||||
print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}")
|
|
||||||
errors += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# allow directories if they contain README.md
|
|
||||||
if resolved.is_dir():
|
|
||||||
if not (resolved / "README.md").is_file():
|
|
||||||
print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}")
|
|
||||||
errors += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not resolved.exists():
|
|
||||||
print(f"[ERROR] Broken link: {f} -> {target}")
|
|
||||||
errors += 1
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft")
|
|
||||||
PY
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# .aiignore - Verhindert Token-Waste für Nolik
|
||||||
|
|
||||||
|
# Abhängigkeiten & Binaries
|
||||||
|
build/
|
||||||
|
.gradle/
|
||||||
|
*.jar
|
||||||
|
*.deb
|
||||||
|
*.msi
|
||||||
|
|
||||||
|
# Sensible Daten (auch lokal!)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
config/docker/certs/
|
||||||
|
*.pem
|
||||||
|
*.jks
|
||||||
|
postgres-data/
|
||||||
|
valkey-data/
|
||||||
|
|
||||||
|
# Doku-Builds (Nolik soll die Source-Files in docs/ lesen, nicht die HTML-Exporte)
|
||||||
|
build/dokka/
|
||||||
|
docs/Neumarkt2026/*.pdf
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||||
|
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
|
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||||
|
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
|
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
|
||||||
@@ -20,7 +20,7 @@ Die gesamte Projektdokumentation (Architektur, Fachdomäne, Entwickler-Anleitung
|
|||||||
| [03_Domain](./docs/03_Domain) | Fachlichkeit, Turnierregeln, Entities |
|
| [03_Domain](./docs/03_Domain) | Fachlichkeit, Turnierregeln, Entities |
|
||||||
| [07_Infrastructure](./docs/07_Infrastructure) | Docker, Keycloak, CI/CD, Zora-Infrastruktur |
|
| [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
|
## 📜 Lizenz
|
||||||
|
|
||||||
Dieses Projekt steht unter der [MIT License](LICENSE).
|
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 io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.testcontainers.containers.GenericContainer
|
import org.testcontainers.containers.GenericContainer
|
||||||
@@ -70,7 +70,7 @@ class ValkeyDistributedCachePerformanceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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" }
|
logger.info { "Starting concurrent access test" }
|
||||||
val numberOfCoroutines = 100
|
val numberOfCoroutines = 100
|
||||||
val operationsPerCoroutine = 50
|
val operationsPerCoroutine = 50
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package at.mocode.zns.importer
|
package at.mocode.zns.importer
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.ReiterLizenz
|
||||||
import at.mocode.masterdata.domain.repository.*
|
import at.mocode.masterdata.domain.repository.*
|
||||||
import at.mocode.zns.parser.ZnsFunktionaerParser
|
import at.mocode.zns.parser.ZnsFunktionaerParser
|
||||||
import at.mocode.zns.parser.ZnsPferdParser
|
import at.mocode.zns.parser.ZnsPferdParser
|
||||||
|
|||||||
+1
-10
@@ -3,6 +3,7 @@
|
|||||||
package at.mocode.masterdata.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
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.model.ReiterLizenzKlasseE
|
||||||
import at.mocode.core.domain.serialization.InstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.LocalDateSerializer
|
import at.mocode.core.domain.serialization.LocalDateSerializer
|
||||||
@@ -14,16 +15,6 @@ import kotlin.time.Clock
|
|||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlin.uuid.Uuid
|
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.
|
* 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.DatenQuelleE
|
||||||
import at.mocode.core.domain.model.ReiterAltersKlasseE
|
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.domain.model.ReiterLizenzKlasseE
|
||||||
import at.mocode.core.utils.database.DatabaseFactory
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
import at.mocode.masterdata.domain.model.Reiter
|
import at.mocode.masterdata.domain.model.Reiter
|
||||||
import at.mocode.masterdata.domain.model.ReiterLizenz
|
|
||||||
import at.mocode.masterdata.domain.repository.ReiterRepository
|
import at.mocode.masterdata.domain.repository.ReiterRepository
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
|||||||
+44
-23
@@ -38,7 +38,7 @@ plugins {
|
|||||||
// ### ALLPROJECTS CONFIGURATION ###
|
// ### ALLPROJECTS CONFIGURATION ###
|
||||||
// ##################################################################
|
// ##################################################################
|
||||||
|
|
||||||
val isWasmEnabled = findProperty("enableWasm")?.toString()?.toBoolean() ?: false
|
val isWasmEnabled: Boolean = findProperty("enableWasm")?.toString()?.toBoolean() ?: false
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// Zentrale Versionierung — liest version.properties (SemVer)
|
// Zentrale Versionierung — liest version.properties (SemVer)
|
||||||
@@ -47,10 +47,10 @@ val versionProps =
|
|||||||
java.util.Properties().also { props ->
|
java.util.Properties().also { props ->
|
||||||
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
||||||
}
|
}
|
||||||
val vMajor = versionProps.getProperty("VERSION_MAJOR", "1")
|
val vMajor: String = versionProps.getProperty("VERSION_MAJOR", "1")
|
||||||
val vMinor = versionProps.getProperty("VERSION_MINOR", "0")
|
val vMinor: String = versionProps.getProperty("VERSION_MINOR", "0")
|
||||||
val vPatch = versionProps.getProperty("VERSION_PATCH", "0")
|
val vPatch: String = versionProps.getProperty("VERSION_PATCH", "0")
|
||||||
val vQualifier = versionProps.getProperty("VERSION_QUALIFIER", "").trim()
|
val vQualifier: String = versionProps.getProperty("VERSION_QUALIFIER", "").trim()
|
||||||
val semVer = if (vQualifier.isBlank()) "$vMajor.$vMinor.$vPatch" else "$vMajor.$vMinor.$vPatch-$vQualifier"
|
val semVer = if (vQualifier.isBlank()) "$vMajor.$vMinor.$vPatch" else "$vMajor.$vMinor.$vPatch-$vQualifier"
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@@ -90,7 +90,7 @@ subprojects {
|
|||||||
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
||||||
// Suppress ByteBuddy/Mockito dynamic agent loading warnings (Java 21+)
|
// Suppress ByteBuddy/Mockito dynamic agent loading warnings (Java 21+)
|
||||||
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
||||||
// Increase test JVM memory with a stable configuration
|
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||||
minHeapSize = "512m"
|
minHeapSize = "512m"
|
||||||
maxHeapSize = "2g"
|
maxHeapSize = "2g"
|
||||||
// Parallel test execution for better performance
|
// Parallel test execution for better performance
|
||||||
@@ -113,7 +113,7 @@ subprojects {
|
|||||||
// (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings)
|
// (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings)
|
||||||
// to avoid compiler-flag incompatibilities across toolchains.
|
// 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.
|
// This significantly reduces build times during Desktop development.
|
||||||
// Flag is defined at the beginning of the script.
|
// Flag is defined at the beginning of the script.
|
||||||
|
|
||||||
@@ -166,6 +166,7 @@ subprojects {
|
|||||||
jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false")
|
jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false")
|
||||||
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
|
||||||
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
jvmArgs("-XX:+EnableDynamicAgentLoading")
|
||||||
|
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||||
maxHeapSize = "2g"
|
maxHeapSize = "2g"
|
||||||
dependsOn("testClasses")
|
dependsOn("testClasses")
|
||||||
}
|
}
|
||||||
@@ -175,20 +176,30 @@ subprojects {
|
|||||||
// Applies to all Exec-based tasks (covers Yarn/NPM invocations used by Kotlin JS plugin)
|
// Applies to all Exec-based tasks (covers Yarn/NPM invocations used by Kotlin JS plugin)
|
||||||
tasks.withType<Exec>().configureEach {
|
tasks.withType<Exec>().configureEach {
|
||||||
// Merge existing NODE_OPTIONS with --no-deprecation
|
// Merge existing NODE_OPTIONS with --no-deprecation
|
||||||
val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
val current: String? = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||||||
val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
val merged: String = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||||||
environment("NODE_OPTIONS", merged)
|
environment("NODE_OPTIONS", merged)
|
||||||
// Also set the legacy switch to silence warnings entirely
|
// Also set the legacy switch to silence warnings entirely
|
||||||
environment("NODE_NO_WARNINGS", "1")
|
environment("NODE_NO_WARNINGS", "1")
|
||||||
// Set a Chrome binary path to avoid snap permission issues
|
// 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("CHROME_BIN", "/usr/bin/google-chrome-stable")
|
||||||
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
||||||
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
// Detekt & Ktlint default setup
|
// 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") {
|
plugins.withId("io.gitlab.arturbosch.detekt") {
|
||||||
extensions.configure(DetektExtension::class.java) {
|
extensions.configure(DetektExtension::class.java) {
|
||||||
buildUponDefaultConfig = true
|
buildUponDefaultConfig = true
|
||||||
@@ -267,7 +278,6 @@ tasks.register("checkBundleBudget") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shells.forEach { shell ->
|
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"
|
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
|
// Budgets are keyed by a Gradle path with colons but without leading colon in config for readability
|
||||||
val budgetKeyCandidates =
|
val budgetKeyCandidates =
|
||||||
@@ -362,8 +372,8 @@ tasks.register("staticAnalysis") {
|
|||||||
|
|
||||||
// Apply Dokka (V2) automatically to Kotlin subprojects
|
// Apply Dokka (V2) automatically to Kotlin subprojects
|
||||||
subprojects {
|
subprojects {
|
||||||
plugins.withId("org.jetbrains.kotlin.jvm") { apply(plugin = "org.jetbrains.dokka") }
|
plugins.withId("org.jetbrains.kotlin.jvm") { pluginManager.apply("org.jetbrains.dokka") }
|
||||||
plugins.withId("org.jetbrains.kotlin.multiplatform") { apply(plugin = "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
|
// Aggregate tasks to build multi-module docs in Markdown (GFM) and HTML
|
||||||
@@ -372,27 +382,36 @@ val dokkaAll =
|
|||||||
tasks.register("dokkaAll") {
|
tasks.register("dokkaAll") {
|
||||||
group = "documentation"
|
group = "documentation"
|
||||||
description = "Builds Dokka (V2) for all modules and aggregates outputs under build/dokka/all"
|
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
|
// PERFORMANCE: Nur ausführen wenn explizit gefordert
|
||||||
dependsOn(
|
enabled = project.hasProperty("runDokka")
|
||||||
|
|
||||||
|
// Capture required values for configuration cache
|
||||||
|
val rootBuildDir = layout.buildDirectory.get().asFile
|
||||||
|
val subprojectData =
|
||||||
subprojects
|
subprojects
|
||||||
.filter { it.plugins.hasPlugin("org.jetbrains.dokka") }
|
.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 {
|
doLast {
|
||||||
val dest = layout.buildDirectory.dir("dokka/all").get().asFile
|
val dest = File(rootBuildDir, "dokka/all")
|
||||||
if (dest.exists()) dest.deleteRecursively()
|
if (dest.exists()) dest.deleteRecursively()
|
||||||
dest.mkdirs()
|
dest.mkdirs()
|
||||||
|
|
||||||
val modules = mutableListOf<Pair<String, String>>()
|
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
|
// 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()) {
|
if (outHtml.exists()) {
|
||||||
val modulePath = p.path.trimStart(':').replace(':', '/')
|
val modulePath = pPath.trimStart(':').replace(':', '/')
|
||||||
val targetDir = File(dest, modulePath)
|
val targetDir = File(dest, modulePath)
|
||||||
outHtml.copyRecursively(targetDir, overwrite = true)
|
outHtml.copyRecursively(targetDir, overwrite = true)
|
||||||
modules.add(p.name to modulePath)
|
modules.add(pName to modulePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,14 +470,16 @@ tasks.register("docs") {
|
|||||||
// Apply Node warning suppression on root project Exec tasks as well
|
// 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
|
// Ensures aggregated Kotlin/JS tasks created at root (e.g., kotlinNpmInstall) inherit the env
|
||||||
tasks.withType<Exec>().configureEach {
|
tasks.withType<Exec>().configureEach {
|
||||||
val current = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
val current: String? = (environment["NODE_OPTIONS"] as String?) ?: System.getenv("NODE_OPTIONS")
|
||||||
val merged = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
val merged: String = if (current.isNullOrBlank()) "--no-deprecation" else "$current --no-deprecation"
|
||||||
environment("NODE_OPTIONS", merged)
|
environment("NODE_OPTIONS", merged)
|
||||||
environment("NODE_NO_WARNINGS", "1")
|
environment("NODE_NO_WARNINGS", "1")
|
||||||
// Set a Chrome binary path to avoid snap permission issues
|
// 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("CHROME_BIN", "/usr/bin/google-chrome-stable")
|
||||||
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
environment("CHROMIUM_BIN", "/usr/bin/chromium")
|
||||||
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
environment("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.wrapper {
|
tasks.wrapper {
|
||||||
|
|||||||
+17
-34
@@ -1,63 +1,46 @@
|
|||||||
# =============================================================================
|
include required("/stdlib/jdk/21/amazon.conf")
|
||||||
# Conveyor Configuration for Meldestelle Desktop App
|
|
||||||
# =============================================================================
|
|
||||||
# Dieser Build-Weg ermöglicht das Cross-Packaging für Windows (MSI) auf Linux.
|
|
||||||
# Dokumentation: https://conveyor.hydraulic.dev/
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
|
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 {
|
app {
|
||||||
# Anzeige-Name und Vendor
|
|
||||||
display-name = "Meldestelle"
|
display-name = "Meldestelle"
|
||||||
rdns-name = "at.mocode.meldestelle"
|
rdns-name = "at.mocode.meldestelle"
|
||||||
vendor = "mo-code.at"
|
vendor = "mo-code.at"
|
||||||
contact-email = "support@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)
|
# Ziel-Plattformen: Windows und Linux
|
||||||
# Für diesen Task hart codiert oder via CLI-Flag --variable übergeben.
|
machines = [ windows.amd64, linux.amd64.glibc ]
|
||||||
version = "1.0.0"
|
|
||||||
|
|
||||||
# Beschreibung
|
site.base-url = "localhost"
|
||||||
description = "ÖTO-konforme Turnier-Meldestelle – Desktop App"
|
|
||||||
|
|
||||||
# Ziel-Plattformen
|
# Icons werden im Ordner gesucht
|
||||||
# Wir konzentrieren uns auf Windows, können aber Linux/Mac später ergänzen.
|
|
||||||
site.base-url = "localhost" # Später echte Update-URL
|
|
||||||
|
|
||||||
# Icons
|
|
||||||
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.png"
|
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.png"
|
||||||
|
|
||||||
# Einbetten der JRE (Temurin 21 wie in CI genutzt)
|
|
||||||
jvm {
|
jvm {
|
||||||
gui {
|
gui {
|
||||||
main-class = "at.mocode.frontend.shell.desktop.MainKt"
|
main-class = "at.mocode.frontend.shell.desktop.MainKt"
|
||||||
}
|
}
|
||||||
|
|
||||||
# JVM-Argumente (analog build.gradle.kts)
|
|
||||||
jvm-options = [
|
jvm-options = [
|
||||||
"-Xms128m",
|
"-Xms256m",
|
||||||
"-Xmx512m",
|
"-Xmx1024m",
|
||||||
"-Dfile.encoding=UTF-8",
|
"-Dfile.encoding=UTF-8",
|
||||||
"--enable-native-access=ALL-UNNAMED"
|
"--enable-native-access=ALL-UNNAMED"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input-Dateien: Hier ziehen wir die Uber-JAR oder die Gradle-Outputs.
|
# JARs aus dem Gradle-Build
|
||||||
# Da wir plattformunabhängig bleiben wollen, nutzen wir das Gradle-Output-Dir.
|
inputs += "frontend/shells/meldestelle-desktop/build/libs/*.jar"
|
||||||
inputs += "frontend/shells/meldestelle-desktop/build/libs/meldestelle-desktop-jvm-*.jar"
|
|
||||||
|
|
||||||
# Windows-spezifische Einstellungen
|
|
||||||
windows {
|
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"
|
upgrade-uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
# Menü-Eintrag
|
|
||||||
menu-group = "Meldestelle"
|
menu-group = "Meldestelle"
|
||||||
# Verknüpfung
|
|
||||||
desktop-shortcut = true
|
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
|
package at.mocode.zns.parser
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
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.model.ReiterLizenzKlasseE
|
||||||
import at.mocode.core.utils.parser.FixedWidthLineReader
|
import at.mocode.core.utils.parser.FixedWidthLineReader
|
||||||
import at.mocode.masterdata.domain.model.Reiter
|
import at.mocode.masterdata.domain.model.Reiter
|
||||||
import at.mocode.masterdata.domain.model.ReiterLizenz
|
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
|||||||
@@ -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] **Client-Konfiguration:** Master kann nun Clients in der UI hinzufügen und bearbeiten.
|
||||||
* [x] **Master-UX:** Konfiguration beim Start nicht mehr zwangsgesperrt.
|
* [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] **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
|
* [ ] **PoC Verifikation:** 🔴 **BLOCKIERT** (Log 483: ARM64-Runner inkompatibel mit Conveyor-Binary; Workflow auf
|
||||||
manuell gesetzt).
|
manuell gesetzt).
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.*
|
||||||
+14
-16
@@ -2,27 +2,25 @@
|
|||||||
|
|
||||||
**Status:** 🏗️ In Arbeit
|
**Status:** 🏗️ In Arbeit
|
||||||
**SCS:** Desktop App / Infrastructure
|
**SCS:** Desktop App / Infrastructure
|
||||||
**Branch:** `feature/desktop-network-chat` (neuer Branch, erstellt ausgehend von `feature/turnier-anlage-wizard`)
|
**Branch:** `feature/desktop-network-chat`
|
||||||
|
|
||||||
## 🎯 Aktuelles Ziel
|
## 🎯 Aktuelles Ziel
|
||||||
1. **Netzwerk-Kommunikation (Chat POC):** Implementierung einer simplen Chat-Funktion für die Desktop-App, die im lokalen Netzwerk funktioniert (Verbindungstest).
|
1. **Stabile Netzwerk-Kommunikation:** Implementierung einer robusten P2P-Kommunikation mit Reconnection-Logik und Heartbeats.
|
||||||
2. **Multi-Node Architektur:** Host-Client-Modell (1..n Hosts, 1..n Clients) vorbereiten. Hosts und Clients müssen in einem lokalen Netzwerk (LAN/WLAN) plattformunabhängig (Windows, Mac, Linux) stabil kommunizieren können.
|
2. **Multi-Node Architektur:** Host-Client-Modell stabilisiert.
|
||||||
3. **Conveyor Build (Pausiert):** Lauffähiger Build der Desktop-App via Conveyor für Windows (.msi/.exe) und Linux. Bereitstellung über Web-App. Wird nach dem Netzwerk-Proof-of-Concept in Angriff genommen.
|
3. **Professional Packaging:** Vorbereitung für echte Installer (.msi, .deb) via Conveyor.
|
||||||
|
|
||||||
## 🛠️ Letzte Änderungen
|
## 🛠️ Letzte Änderungen
|
||||||
- Fokus auf Netzwerk- & Offline-Fähigkeiten gelegt. Turnier-Anlage-Wizard pausiert.
|
- **Hardening P2P:** `JvmP2pSyncService` komplett refactored. Jetzt mit automatischem Reconnect (3s Intervall) und Ktor Heartbeats (Ping/Pong alle 5s).
|
||||||
- Neuer Branch `feature/desktop-network-chat` für die anstehenden Arbeiten.
|
- **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-Bereiche
|
## 📍 Fokus-Bereiche
|
||||||
- Lokale Netzwerk-Discovery (z.B. Ktor, UDP Broadcast, mDNS).
|
- [x] Robuste Reconnection-Logik im P2P Service.
|
||||||
- P2P oder Client-Server Chat-Kommunikation im lokalen Netzwerk für den Verbindungs-Check.
|
- [x] Heartbeats zur Erkennung toter Verbindungen.
|
||||||
- KMP Desktop-Modul.
|
- [ ] In-App Feedback bei Firewall-Blockaden.
|
||||||
|
- [ ] Multi-Node Test mit > 2 Teilnehmern.
|
||||||
## 🚧 Offene Punkte / Blocker
|
|
||||||
- [ ] Konzept für Host/Client-Discovery im lokalen Netz umsetzen.
|
|
||||||
- [ ] Implementierung eines lokalen Chat-Moduls in der Desktop-App (Linux/Desktop-Test).
|
|
||||||
- [ ] Erfolgreicher Conveyor Build für Windows & Linux (Später).
|
|
||||||
|
|
||||||
## 🔄 Nächste Schritte
|
## 🔄 Nächste Schritte
|
||||||
- [ ] Architektur-Entscheidung (ADR) für lokale Netzwerk-Discovery und Kommunikation treffen (Ktor Sockets, UDP, etc.).
|
- [ ] Multi-Node Stabilitätstest (Simulierte Netzwerk-Drops).
|
||||||
- [ ] Erste Implementierung des Discovery-Mechanismus.
|
- [ ] 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
|
||||||
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.
@@ -60,6 +60,16 @@ sqldelight {
|
|||||||
create("AppDatabase") {
|
create("AppDatabase") {
|
||||||
packageName.set("at.mocode.frontend.core.localdb")
|
packageName.set("at.mocode.frontend.core.localdb")
|
||||||
generateAsync.set(true)
|
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
|
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(
|
class DatabaseProvider(
|
||||||
private val driverFactory: DatabaseDriverFactory
|
private val driverFactory: DatabaseDriverFactory
|
||||||
) {
|
) {
|
||||||
@@ -17,9 +11,6 @@ class DatabaseProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Koin module to provide the SQLDelight database for all frontend targets.
|
|
||||||
*/
|
|
||||||
val localDbModule = module {
|
val localDbModule = module {
|
||||||
single<DatabaseDriverFactory> { DatabaseDriverFactory() }
|
single<DatabaseDriverFactory> { DatabaseDriverFactory() }
|
||||||
single<DatabaseProvider> { DatabaseProvider(get()) }
|
single<DatabaseProvider> { DatabaseProvider(get()) }
|
||||||
|
|||||||
+10
-4
@@ -22,8 +22,10 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
|||||||
private val registeredSet = ConcurrentHashMap.newKeySet<String>() // key: "${name}@${addr.hostAddress}:$port"
|
private val registeredSet = ConcurrentHashMap.newKeySet<String>() // key: "${name}@${addr.hostAddress}:$port"
|
||||||
|
|
||||||
// Debounce/Guards
|
// Debounce/Guards
|
||||||
@Volatile private var lastStartRequestedAt: Long = 0L
|
@Volatile
|
||||||
@Volatile private var lastStartIp: String? = null
|
private var lastStartRequestedAt: Long = 0L
|
||||||
|
@Volatile
|
||||||
|
private var lastStartIp: String? = null
|
||||||
|
|
||||||
private val _discoveredServices = MutableStateFlow<List<DiscoveredService>>(emptyList())
|
private val _discoveredServices = MutableStateFlow<List<DiscoveredService>>(emptyList())
|
||||||
override val discoveredServices: StateFlow<List<DiscoveredService>> = _discoveredServices.asStateFlow()
|
override val discoveredServices: StateFlow<List<DiscoveredService>> = _discoveredServices.asStateFlow()
|
||||||
@@ -149,7 +151,10 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
|||||||
val name = iface.name.lowercase()
|
val name = iface.name.lowercase()
|
||||||
// Filtere Docker/Bridged/VETH/VM-Schnittstellen heraus
|
// Filtere Docker/Bridged/VETH/VM-Schnittstellen heraus
|
||||||
if (iface.isLoopback || !iface.isUp || iface.isVirtual) continue
|
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
|
if (name.startsWith("br-") || name.startsWith("docker") || name.startsWith("veth") || name.contains("vmnet") || name.contains(
|
||||||
|
"virbr"
|
||||||
|
)
|
||||||
|
) continue
|
||||||
|
|
||||||
val inetAddresses = iface.inetAddresses
|
val inetAddresses = iface.inetAddresses
|
||||||
while (inetAddresses.hasMoreElements()) {
|
while (inetAddresses.hasMoreElements()) {
|
||||||
@@ -172,7 +177,8 @@ class JmDnsDiscoveryService : NetworkDiscoveryService {
|
|||||||
// Bevorzuge private LAN IPv4 (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
// Bevorzuge private LAN IPv4 (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||||
fun isPrivateIPv4(a: InetAddress): Boolean {
|
fun isPrivateIPv4(a: InetAddress): Boolean {
|
||||||
val h = a.hostAddress
|
val h = a.hostAddress
|
||||||
return h.startsWith("192.168.") || h.startsWith("10.") || (h.startsWith("172.") && h.split('.').getOrNull(1)?.toIntOrNull() in 16..31)
|
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) }
|
return addresses.sortedWith(compareByDescending<InetAddress> { isPrivateIPv4(it) }
|
||||||
.thenBy { it.hostAddress })
|
.thenBy { it.hostAddress })
|
||||||
|
|||||||
+69
-33
@@ -2,33 +2,41 @@ package at.mocode.frontend.core.network.sync
|
|||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.plugins.websocket.*
|
import io.ktor.client.plugins.websocket.*
|
||||||
|
import io.ktor.client.plugins.websocket.WebSockets
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.netty.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import io.ktor.server.websocket.*
|
import io.ktor.server.websocket.*
|
||||||
import io.ktor.websocket.*
|
import io.ktor.websocket.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
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 {
|
class JvmP2pSyncService : P2pSyncService {
|
||||||
companion object {
|
companion object {
|
||||||
// Prozessweiter, portbasierter Guard gegen Mehrfachstart
|
|
||||||
private val startedPorts: MutableSet<Int> = ConcurrentHashMap.newKeySet()
|
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 var server: EmbeddedServer<*, *>? = null
|
private var server: EmbeddedServer<*, *>? = null
|
||||||
private var currentPort: Int? = null
|
private var currentPort: Int? = null
|
||||||
private val client = HttpClient {
|
private val client = HttpClient {
|
||||||
install(io.ktor.client.plugins.websocket.WebSockets)
|
install(WebSockets) {
|
||||||
|
pingInterval = PING_INTERVAL_MS.milliseconds
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _incomingEvents = MutableSharedFlow<SyncEvent>()
|
private val _incomingEvents = MutableSharedFlow<SyncEvent>(extraBufferCapacity = 64)
|
||||||
override val incomingEvents: Flow<SyncEvent> = _incomingEvents.asSharedFlow()
|
override val incomingEvents: Flow<SyncEvent> = _incomingEvents.asSharedFlow()
|
||||||
|
|
||||||
private val activeSessions = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
|
private val activeSessions = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
|
||||||
@@ -36,26 +44,29 @@ class JvmP2pSyncService : P2pSyncService {
|
|||||||
override val connectedPeers: Flow<List<String>> = _connectedPeers.asStateFlow()
|
override val connectedPeers: Flow<List<String>> = _connectedPeers.asStateFlow()
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
private val connectionJobs = ConcurrentHashMap<String, Job>()
|
||||||
|
|
||||||
override fun startServer(port: Int) {
|
override fun startServer(port: Int) {
|
||||||
// Instanz-Guard (gleiche Instanz)
|
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
println("[P2P Server] Bereits gestartet (Instanz) auf Port ${currentPort ?: port} – idempotent")
|
println("[P2P Server] Bereits aktiv auf Port ${currentPort ?: port}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prozessweiter, portbasierter Guard
|
|
||||||
if (!startedPorts.add(port)) {
|
if (!startedPorts.add(port)) {
|
||||||
println("[P2P Server] Bereits gestartet (Prozess) auf Port $port – idempotent, kein neuer Bind")
|
println("[P2P Server] Port $port wird bereits von einer anderen Instanz genutzt.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
server = embeddedServer(Netty, port = port) {
|
server = embeddedServer(Netty, port = port, host = "0.0.0.0") {
|
||||||
install(io.ktor.server.websocket.WebSockets)
|
install(io.ktor.server.websocket.WebSockets) {
|
||||||
|
pingPeriod = PING_INTERVAL_MS.milliseconds
|
||||||
|
timeout = PING_TIMEOUT_MS.milliseconds
|
||||||
|
}
|
||||||
routing {
|
routing {
|
||||||
webSocket("/sync") {
|
webSocket("/sync") {
|
||||||
println("[P2P Server] Neuer Peer verbunden")
|
val remote = call.request.local.remoteAddress
|
||||||
|
println("[P2P Server] Neuer Peer verbunden: $remote")
|
||||||
activeSessions.add(this)
|
activeSessions.add(this)
|
||||||
updatePeers()
|
updatePeers()
|
||||||
try {
|
try {
|
||||||
@@ -65,46 +76,56 @@ class JvmP2pSyncService : P2pSyncService {
|
|||||||
try {
|
try {
|
||||||
val event = Json.decodeFromString<SyncEvent>(text)
|
val event = Json.decodeFromString<SyncEvent>(text)
|
||||||
_incomingEvents.emit(event)
|
_incomingEvents.emit(event)
|
||||||
} catch (e: Exception) {
|
} catch (ex: Exception) {
|
||||||
println("[P2P Server] Fehler beim Dekodieren: ${e.message}")
|
println("[P2P Server] Fehler beim Dekodieren von $remote: ${ex.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
println("[P2P Server] Verbindung zu $remote unterbrochen: ${ex.message}")
|
||||||
} finally {
|
} finally {
|
||||||
activeSessions.remove(this)
|
activeSessions.remove(this)
|
||||||
updatePeers()
|
updatePeers()
|
||||||
println("[P2P Server] Peer getrennt")
|
println("[P2P Server] Peer $remote getrennt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start(wait = false)
|
}.start(wait = false)
|
||||||
currentPort = port
|
currentPort = port
|
||||||
println("[P2P Server] Gestartet auf Port $port")
|
println("[P2P Server] Erfolgreich gestartet auf Port $port")
|
||||||
} catch (e: Exception) {
|
} catch (ex: Exception) {
|
||||||
// Start fehlgeschlagen -> Port-Lock wieder freigeben
|
|
||||||
startedPorts.remove(port)
|
startedPorts.remove(port)
|
||||||
server = null
|
server = null
|
||||||
currentPort = null
|
currentPort = null
|
||||||
println("[P2P Server] Start auf Port $port fehlgeschlagen: ${e.message}")
|
println("[P2P Server] Fehler beim Starten des Servers auf Port $port: ${ex.message}")
|
||||||
throw e
|
throw ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopServer() {
|
override fun stopServer() {
|
||||||
|
connectionJobs.values.forEach { it.cancel() }
|
||||||
|
connectionJobs.clear()
|
||||||
try {
|
try {
|
||||||
server?.stop(1000, 2000)
|
server?.stop(1000, 2000)
|
||||||
} finally {
|
} finally {
|
||||||
server = null
|
server = null
|
||||||
currentPort?.let { startedPorts.remove(it) }
|
currentPort?.let { startedPorts.remove(it) }
|
||||||
currentPort = null
|
currentPort = null
|
||||||
|
println("[P2P Server] Server gestoppt.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun connectToPeer(host: String, port: Int) {
|
override suspend fun connectToPeer(host: String, port: Int) {
|
||||||
scope.launch {
|
val peerKey = "$host:$port"
|
||||||
|
|
||||||
|
connectionJobs[peerKey]?.cancel()
|
||||||
|
|
||||||
|
val job = scope.launch {
|
||||||
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
|
println("[P2P Client] Verbindungsversuch zu $peerKey...")
|
||||||
client.webSocket(host = host, port = port, path = "/sync") {
|
client.webSocket(host = host, port = port, path = "/sync") {
|
||||||
println("[P2P Client] Verbunden mit $host:$port")
|
println("[P2P Client] Verbunden mit $peerKey")
|
||||||
activeSessions.add(this)
|
activeSessions.add(this)
|
||||||
updatePeers()
|
updatePeers()
|
||||||
try {
|
try {
|
||||||
@@ -115,32 +136,47 @@ class JvmP2pSyncService : P2pSyncService {
|
|||||||
_incomingEvents.emit(event)
|
_incomingEvents.emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
println("[P2P Client] Verbindung zu $peerKey abgebrochen: ${ex.message}")
|
||||||
} finally {
|
} finally {
|
||||||
activeSessions.remove(this)
|
activeSessions.remove(this)
|
||||||
updatePeers()
|
updatePeers()
|
||||||
println("[P2P Client] Verbindung zu $host:$port beendet")
|
println("[P2P Client] Session mit $peerKey beendet.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (ex: Exception) {
|
||||||
println("[P2P Client] Fehler bei Verbindung zu $host:$port: ${e.message}")
|
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) {
|
override suspend fun broadcastEvent(event: SyncEvent) {
|
||||||
val text = Json.encodeToString(event)
|
val text = Json.encodeToString(event)
|
||||||
activeSessions.toList().forEach { session ->
|
val sessions = activeSessions.toList()
|
||||||
|
sessions.forEach { session ->
|
||||||
try {
|
try {
|
||||||
|
if (session.isActive) {
|
||||||
session.send(Frame.Text(text))
|
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() {
|
private fun updatePeers() {
|
||||||
// Da wir keine einfachen IPs in den Sessions haben ohne tieferes Casting,
|
_connectedPeers.value = activeSessions.map { session ->
|
||||||
// nutzen wir hier erst mal einen Platzhalter oder zählen nur.
|
when (session) {
|
||||||
_connectedPeers.value = activeSessions.map { "Peer-${it.hashCode()}" }
|
is DefaultWebSocketServerSession -> session.call.request.local.remoteAddress
|
||||||
|
else -> "Outgoing-Peer"
|
||||||
|
}
|
||||||
|
}.distinct()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package at.mocode.frontend.core.network.sync
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wasm-spezifische Implementierung (vorerst No-op).
|
* 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.DeviceInitializationValidator
|
||||||
import at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting
|
import at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting
|
||||||
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
|
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DiscoveryRadar(
|
private fun DiscoveryRadar(
|
||||||
@@ -94,7 +96,7 @@ fun DeviceInitializationScreen(
|
|||||||
// Automatische Discovery starten
|
// Automatische Discovery starten
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.startDiscovery()
|
viewModel.startDiscovery()
|
||||||
roleSelectorFocus.requestFocus()
|
delay(100.milliseconds); withFrameMillis { roleSelectorFocus.requestFocus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
+62
-27
@@ -48,12 +48,6 @@ actual fun DeviceInitializationConfig(
|
|||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val (_, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() }
|
val (_, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (settings.deviceName.isEmpty()) {
|
|
||||||
deviceNameFocus.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = MaterialTheme.shapes.large,
|
shape = MaterialTheme.shapes.large,
|
||||||
@@ -66,7 +60,7 @@ actual fun DeviceInitializationConfig(
|
|||||||
value = settings.deviceName,
|
value = settings.deviceName,
|
||||||
onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } },
|
onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } },
|
||||||
label = "Gerätename",
|
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",
|
placeholder = "z.B. Meldestelle-PC-1",
|
||||||
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
||||||
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
||||||
@@ -77,24 +71,43 @@ actual fun DeviceInitializationConfig(
|
|||||||
compact = true
|
compact = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// NETZWERK-INTERFACES (EXPERTEN-MODUS)
|
|
||||||
val interfaces = remember {
|
val interfaces = remember {
|
||||||
NetworkInterface.getNetworkInterfaces().toList()
|
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 ->
|
.map { ni ->
|
||||||
val friendlyName = when {
|
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("wlan", ignoreCase = true) || ni.displayName.contains(
|
||||||
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"
|
"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
|
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
|
?: ni.inetAddresses.nextElement().hostAddress
|
||||||
|
|
||||||
val isConnected = !ni.isLoopback && ni.isUp && ni.interfaceAddresses.any {
|
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(
|
Surface(
|
||||||
onClick = { if (!uiState.isLocked) viewModel.updateSettings { s -> s.copy(networkInterface = info.id) } },
|
onClick = { if (!uiState.isLocked) viewModel.updateSettings { s -> s.copy(networkInterface = info.id) } },
|
||||||
shape = MaterialTheme.shapes.medium,
|
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,
|
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
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))
|
Spacer(Modifier.width(12.dp))
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(info.name, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
|
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) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = settings.sharedKey,
|
value = settings.sharedKey,
|
||||||
onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } },
|
onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } },
|
||||||
label = "Sicherheitsschlüssel (Sync-Key)",
|
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",
|
placeholder = "Mindestens 8 Zeichen",
|
||||||
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
|
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
|
||||||
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
|
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
|
||||||
@@ -157,7 +174,6 @@ actual fun DeviceInitializationConfig(
|
|||||||
compact = true
|
compact = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// CLIENT-VERBINDUNG-FEEDBACK
|
|
||||||
if (settings.networkRole == NetworkRole.CLIENT && !uiState.isLocked) {
|
if (settings.networkRole == NetworkRole.CLIENT && !uiState.isLocked) {
|
||||||
val masterSelected = uiState.selectedMaster != null
|
val masterSelected = uiState.selectedMaster != null
|
||||||
val canConnect = masterSelected && settings.sharedKey.isNotBlank()
|
val canConnect = masterSelected && settings.sharedKey.isNotBlank()
|
||||||
@@ -170,13 +186,19 @@ actual fun DeviceInitializationConfig(
|
|||||||
else -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f)
|
else -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f)
|
||||||
},
|
},
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
border = BorderStroke(1.dp, when (uiState.connectionStatus) {
|
border = BorderStroke(
|
||||||
|
1.dp, when (uiState.connectionStatus) {
|
||||||
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50)
|
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50)
|
||||||
ConnectionStatus.FAILED -> Color(0xFFF44336)
|
ConnectionStatus.FAILED -> Color(0xFFF44336)
|
||||||
else -> MaterialTheme.colorScheme.outlineVariant
|
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)) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
when (uiState.connectionStatus) {
|
when (uiState.connectionStatus) {
|
||||||
ConnectionStatus.CONNECTING -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
ConnectionStatus.CONNECTING -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||||
@@ -210,7 +232,6 @@ actual fun DeviceInitializationConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BACKUP & DRUCKER
|
|
||||||
MsFilePicker(
|
MsFilePicker(
|
||||||
label = "Backup-Verzeichnis (Plan-USB)",
|
label = "Backup-Verzeichnis (Plan-USB)",
|
||||||
selectedPath = settings.backupPath,
|
selectedPath = settings.backupPath,
|
||||||
@@ -246,10 +267,13 @@ actual fun DeviceInitializationConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MASTER: ERWARTETE CLIENTS
|
|
||||||
if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) {
|
if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) {
|
||||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
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)
|
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
||||||
TextButton(onClick = { viewModel.addExpectedClient() }) {
|
TextButton(onClick = { viewModel.addExpectedClient() }) {
|
||||||
Icon(Icons.Default.Add, null, Modifier.size(18.dp))
|
Icon(Icons.Default.Add, null, Modifier.size(18.dp))
|
||||||
@@ -295,7 +319,12 @@ actual fun DeviceInitializationConfig(
|
|||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
IconButton(onClick = { viewModel.removeExpectedClient(index) }) {
|
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)),
|
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
|
||||||
@@ -307,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
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
/**
|
|
||||||
* Dieses Modul kapselt die gesamte UI und Logik für das Ping-Feature.
|
|
||||||
*/
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.composeMultiplatform)
|
alias(libs.plugins.composeMultiplatform)
|
||||||
@@ -17,14 +14,9 @@ version = "1.0.0"
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
binaries.library()
|
binaries.library()
|
||||||
browser {
|
browser { testTask { enabled = false } }
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -33,12 +25,13 @@ kotlin {
|
|||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.sync)
|
implementation(projects.frontend.core.sync)
|
||||||
implementation(projects.frontend.core.localDb)
|
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(libs.sqldelight.coroutines)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
// Explizite Compose-Abhängigkeiten zur Vermeidung von Gradle 10 Warnungen
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
implementation(compose.ui)
|
implementation(compose.ui)
|
||||||
implementation(compose.components.resources)
|
implementation(compose.components.resources)
|
||||||
@@ -49,7 +42,7 @@ kotlin {
|
|||||||
implementation(libs.bundles.compose.common)
|
implementation(libs.bundles.compose.common)
|
||||||
|
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose) // Added koin.compose for koinInject
|
implementation(libs.koin.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
@@ -72,6 +65,5 @@ kotlin {
|
|||||||
wasmJsMain.dependencies {
|
wasmJsMain.dependencies {
|
||||||
implementation(libs.kotlin.stdlib.wasm.js)
|
implementation(libs.kotlin.stdlib.wasm.js)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,6 @@
|
|||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
import java.util.*
|
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 {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.composeCompiler)
|
alias(libs.plugins.composeCompiler)
|
||||||
@@ -26,16 +11,12 @@ plugins {
|
|||||||
group = "at.mocode.frontend.shell"
|
group = "at.mocode.frontend.shell"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Version aus root version.properties lesen (SemVer)
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
val versionProps = Properties().also { props ->
|
val versionProps = Properties().also { props ->
|
||||||
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
rootProject.file("version.properties").inputStream().use { props.load(it) }
|
||||||
}
|
}
|
||||||
val vMajor: String? = versionProps.getProperty("VERSION_MAJOR", "1")
|
val vMajor: String? = versionProps.getProperty("VERSION_MAJOR", "1")
|
||||||
val vMinor: String? = versionProps.getProperty("VERSION_MINOR", "0")
|
val vMinor: String? = versionProps.getProperty("VERSION_MINOR", "0")
|
||||||
val vPatch: String? = versionProps.getProperty("VERSION_PATCH", "0")
|
val vPatch: String? = versionProps.getProperty("VERSION_PATCH", "0")
|
||||||
// nativeDistributions erwartet reines "MAJOR.MINOR.PATCH" (kein Qualifier)
|
|
||||||
val packageVer = "$vMajor.$vMinor.$vPatch"
|
val packageVer = "$vMajor.$vMinor.$vPatch"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -43,7 +24,6 @@ kotlin {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
jvmMain.dependencies {
|
jvmMain.dependencies {
|
||||||
// Core-Module
|
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
@@ -54,10 +34,8 @@ kotlin {
|
|||||||
implementation(projects.frontend.core.auth)
|
implementation(projects.frontend.core.auth)
|
||||||
implementation(projects.core.znsParser)
|
implementation(projects.core.znsParser)
|
||||||
|
|
||||||
// Feature-Module
|
|
||||||
implementation(projects.frontend.features.pingFeature)
|
implementation(projects.frontend.features.pingFeature)
|
||||||
implementation(projects.frontend.features.nennungFeature)
|
implementation(projects.frontend.features.nennungFeature)
|
||||||
|
|
||||||
implementation(projects.frontend.features.znsImportFeature)
|
implementation(projects.frontend.features.znsImportFeature)
|
||||||
implementation(projects.frontend.features.veranstalterFeature)
|
implementation(projects.frontend.features.veranstalterFeature)
|
||||||
implementation(projects.frontend.features.veranstaltungFeature)
|
implementation(projects.frontend.features.veranstaltungFeature)
|
||||||
@@ -70,7 +48,6 @@ kotlin {
|
|||||||
implementation(projects.frontend.features.billingFeature)
|
implementation(projects.frontend.features.billingFeature)
|
||||||
implementation(projects.frontend.features.deviceInitialization)
|
implementation(projects.frontend.features.deviceInitialization)
|
||||||
|
|
||||||
// Compose Desktop
|
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
@@ -80,17 +57,13 @@ kotlin {
|
|||||||
implementation(compose.uiTooling)
|
implementation(compose.uiTooling)
|
||||||
implementation(libs.composeHotReloadApi)
|
implementation(libs.composeHotReloadApi)
|
||||||
|
|
||||||
// DI (Koin)
|
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
|
||||||
// Coroutines
|
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
|
|
||||||
// Bundles
|
|
||||||
implementation(libs.bundles.kmp.common)
|
implementation(libs.bundles.kmp.common)
|
||||||
implementation(libs.bundles.compose.common)
|
implementation(libs.bundles.compose.common)
|
||||||
|
implementation(libs.logback.classic)
|
||||||
}
|
}
|
||||||
|
|
||||||
jvmTest.dependencies {
|
jvmTest.dependencies {
|
||||||
@@ -104,12 +77,8 @@ compose.desktop {
|
|||||||
mainClass = "at.mocode.frontend.shell.desktop.MainKt"
|
mainClass = "at.mocode.frontend.shell.desktop.MainKt"
|
||||||
|
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
// Ziel-Formate: Linux .deb, Windows .msi, macOS .dmg
|
targetFormats(TargetFormat.Deb, TargetFormat.Rpm, TargetFormat.Msi, TargetFormat.Dmg)
|
||||||
targetFormats(TargetFormat.Deb, TargetFormat.Msi, TargetFormat.Dmg)
|
|
||||||
|
|
||||||
// -------------------------------------------------------
|
|
||||||
// Gemeinsame App-Metadaten
|
|
||||||
// -------------------------------------------------------
|
|
||||||
packageName = "meldestelle"
|
packageName = "meldestelle"
|
||||||
packageVersion = packageVer
|
packageVersion = packageVer
|
||||||
description = "ÖTO-konforme Turnier-Meldestelle – Desktop App"
|
description = "ÖTO-konforme Turnier-Meldestelle – Desktop App"
|
||||||
@@ -117,53 +86,30 @@ compose.desktop {
|
|||||||
copyright = "© 2024–2026 mo-code.at. Alle Rechte vorbehalten."
|
copyright = "© 2024–2026 mo-code.at. Alle Rechte vorbehalten."
|
||||||
licenseFile.set(rootProject.file("LICENSE"))
|
licenseFile.set(rootProject.file("LICENSE"))
|
||||||
|
|
||||||
// -------------------------------------------------------
|
|
||||||
// Linux (.deb)
|
|
||||||
// -------------------------------------------------------
|
|
||||||
linux {
|
linux {
|
||||||
// PNG 512×512 px — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
|
|
||||||
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
||||||
packageName = "meldestelle"
|
packageName = "meldestelle"
|
||||||
// Debian-Kategorie
|
|
||||||
appCategory = "misc"
|
appCategory = "misc"
|
||||||
// Menü-Eintrag
|
|
||||||
menuGroup = "Meldestelle"
|
menuGroup = "Meldestelle"
|
||||||
shortcut = true
|
shortcut = true
|
||||||
debMaintainer = "support@mo-code.at"
|
debMaintainer = "support@mo-code.at"
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------
|
|
||||||
// Windows (.msi)
|
|
||||||
// -------------------------------------------------------
|
|
||||||
windows {
|
windows {
|
||||||
// ICO Multi-Size — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
|
|
||||||
iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
|
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"
|
upgradeUuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
menuGroup = "Meldestelle"
|
menuGroup = "Meldestelle"
|
||||||
// Startmenü-Verknüpfung
|
|
||||||
shortcut = true
|
shortcut = true
|
||||||
// Desktop-Verknüpfung
|
|
||||||
dirChooser = true
|
dirChooser = true
|
||||||
perUserInstall = false
|
perUserInstall = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------
|
|
||||||
// macOS (.dmg)
|
|
||||||
// -------------------------------------------------------
|
|
||||||
macOS {
|
macOS {
|
||||||
// ICNS 1024×1024 px — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
|
|
||||||
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
||||||
bundleID = "at.mocode.meldestelle"
|
bundleID = "at.mocode.meldestelle"
|
||||||
appCategory = "public.app-category.productivity"
|
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(
|
modules(
|
||||||
"java.base",
|
"java.base",
|
||||||
"java.desktop",
|
"java.desktop",
|
||||||
@@ -176,8 +122,8 @@ compose.desktop {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JVM-Argumente für die gepackte Anwendung
|
|
||||||
jvmArgs(
|
jvmArgs(
|
||||||
|
"--enable-native-access=ALL-UNNAMED",
|
||||||
"-Xms128m",
|
"-Xms128m",
|
||||||
"-Xmx512m",
|
"-Xmx512m",
|
||||||
"-Dfile.encoding=UTF-8",
|
"-Dfile.encoding=UTF-8",
|
||||||
|
|||||||
+3
-2
@@ -26,10 +26,11 @@ import org.koin.compose.viewmodel.koinViewModel
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DesktopApp() {
|
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 deviceSettings by deviceInitViewModel.uiState.collectAsState()
|
||||||
|
|
||||||
val isDark = when(deviceSettings.settings.appTheme) {
|
val isDark = when (deviceSettings.settings.appTheme) {
|
||||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.SYSTEM -> isSystemInDarkTheme()
|
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.SYSTEM -> isSystemInDarkTheme()
|
||||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.LIGHT -> false
|
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.LIGHT -> false
|
||||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.DARK -> true
|
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.DARK -> true
|
||||||
|
|||||||
-2
@@ -35,8 +35,6 @@ private fun PreviewContent() {
|
|||||||
// --- VEREIN ---
|
// --- VEREIN ---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
||||||
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
||||||
// VeranstalterNeuScreen(onBack = {}, onSave = {})
|
// 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.CurrentUserProvider
|
||||||
import at.mocode.frontend.core.navigation.DeepLinkHandler
|
import at.mocode.frontend.core.navigation.DeepLinkHandler
|
||||||
import at.mocode.frontend.core.navigation.NavigationPort
|
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.navigation.DesktopNavigationPort
|
||||||
import at.mocode.frontend.shell.desktop.repository.DesktopMasterdataRepository
|
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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,4 +39,6 @@ val desktopModule = module {
|
|||||||
single<CurrentUserProvider> { DesktopCurrentUserProvider(get()) }
|
single<CurrentUserProvider> { DesktopCurrentUserProvider(get()) }
|
||||||
single { DeepLinkHandler(get(), get()) }
|
single { DeepLinkHandler(get(), get()) }
|
||||||
single<MasterdataRepository> { DesktopMasterdataRepository(get()) }
|
single<MasterdataRepository> { DesktopMasterdataRepository(get()) }
|
||||||
|
single<VeranstaltungRepository> { StoreVeranstaltungRepository() }
|
||||||
|
viewModel { ChatViewModel(get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-68
@@ -1,17 +1,12 @@
|
|||||||
package at.mocode.frontend.shell.desktop
|
package at.mocode.frontend.shell.desktop
|
||||||
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.WindowState
|
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import at.mocode.frontend.core.auth.di.authModule
|
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.DatabaseProvider
|
||||||
import at.mocode.frontend.core.localdb.localDbModule
|
import at.mocode.frontend.core.localdb.localDbModule
|
||||||
import at.mocode.frontend.core.network.NetworkConfig
|
|
||||||
import at.mocode.frontend.core.network.chat.KtorWebSocketServerService
|
|
||||||
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
|
|
||||||
import at.mocode.frontend.core.network.networkModule
|
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.core.sync.di.syncModule
|
||||||
import at.mocode.frontend.features.billing.di.billingModule
|
import at.mocode.frontend.features.billing.di.billingModule
|
||||||
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
|
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
|
||||||
@@ -25,86 +20,52 @@ import at.mocode.frontend.features.turnier.di.turnierFeatureModule
|
|||||||
import at.mocode.frontend.features.veranstalter.di.veranstalterModule
|
import at.mocode.frontend.features.veranstalter.di.veranstalterModule
|
||||||
import at.mocode.frontend.features.verein.di.vereinFeatureModule
|
import at.mocode.frontend.features.verein.di.vereinFeatureModule
|
||||||
import at.mocode.frontend.features.zns.import.di.znsImportModule
|
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.frontend.shell.desktop.di.desktopModule
|
||||||
import at.mocode.veranstaltung.feature.di.veranstaltungModule
|
import at.mocode.veranstaltung.feature.di.veranstaltungModule
|
||||||
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
|
|
||||||
import kotlinx.coroutines.runBlocking
|
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.core.context.startKoin
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
fun main() = application {
|
fun main() {
|
||||||
try {
|
application {
|
||||||
startKoin {
|
// Koin Starten
|
||||||
|
val koinApp = startKoin {
|
||||||
|
printLogger()
|
||||||
modules(
|
modules(
|
||||||
networkModule,
|
networkModule,
|
||||||
syncModule,
|
syncModule,
|
||||||
authModule,
|
authModule,
|
||||||
localDbModule,
|
localDbModule,
|
||||||
pingFeatureModule,
|
|
||||||
nennungFeatureModule,
|
|
||||||
znsImportModule,
|
|
||||||
profileModule,
|
|
||||||
billingModule,
|
|
||||||
pferdeModule,
|
|
||||||
reiterModule,
|
|
||||||
funktionaerModule,
|
|
||||||
vereinFeatureModule,
|
|
||||||
veranstalterModule,
|
|
||||||
turnierFeatureModule,
|
|
||||||
veranstaltungModule,
|
|
||||||
module {
|
|
||||||
single<VeranstaltungRepository> { StoreVeranstaltungRepository() }
|
|
||||||
},
|
|
||||||
deviceInitializationModule,
|
|
||||||
desktopModule,
|
desktopModule,
|
||||||
|
deviceInitializationModule,
|
||||||
|
billingModule,
|
||||||
|
funktionaerModule,
|
||||||
|
nennungFeatureModule,
|
||||||
|
pferdeModule,
|
||||||
|
pingFeatureModule,
|
||||||
|
profileModule,
|
||||||
|
reiterModule,
|
||||||
|
turnierFeatureModule,
|
||||||
|
veranstalterModule,
|
||||||
|
veranstaltungModule,
|
||||||
|
vereinFeatureModule,
|
||||||
|
znsImportModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
println("[DesktopApp] KOIN initialisiert")
|
|
||||||
// Base URL Log für schnelle Fehlerdiagnose
|
|
||||||
println("[Network] baseUrl=${NetworkConfig.baseUrl}")
|
|
||||||
|
|
||||||
// Starte Netzwerk-Dienste für den POC
|
val koin = koinApp.koin
|
||||||
val koin = GlobalContext.get()
|
|
||||||
try {
|
|
||||||
val wsServer = koin.get<KtorWebSocketServerService>()
|
|
||||||
wsServer.start()
|
|
||||||
val discovery = koin.get<NetworkDiscoveryService>()
|
|
||||||
discovery.startDiscovery()
|
|
||||||
// Im Host-Modus würden wir hier registerService aufrufen.
|
|
||||||
// Für den POC registrieren wir den lokalen Host-Dienst immer mit dem WS-Port
|
|
||||||
try {
|
|
||||||
discovery.registerService(wsServer.getPort())
|
|
||||||
println("[DesktopApp] Discovery-Registrierung durchgeführt (Port ${wsServer.getPort()})")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[DesktopApp] Discovery-Registrierung fehlgeschlagen: ${e.message}")
|
|
||||||
}
|
|
||||||
} catch(e: Exception) {
|
|
||||||
println("[DesktopApp] POC-Dienste konnten nicht gestartet werden: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Testdaten für Prototyp laden
|
// Datenbank initialisieren und als Singleton registrieren
|
||||||
at.mocode.frontend.shell.desktop.data.Store.seed()
|
val dbProvider: DatabaseProvider = koin.get()
|
||||||
} catch (e: Exception) {
|
val database = runBlocking { dbProvider.createDatabase() }
|
||||||
println("[DesktopApp] Koin-Warnung: ${e.message}")
|
koin.loadModules(listOf(module { single { database } }))
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// SyncManager initialisieren und starten (Default Port 8080)
|
||||||
val provider = GlobalContext.get().get<DatabaseProvider>()
|
val syncManager: SyncManager = koin.get()
|
||||||
val db = runBlocking { provider.createDatabase() }
|
syncManager.start(8080)
|
||||||
loadKoinModules(module { single<AppDatabase> { db } })
|
|
||||||
println("[DesktopApp] Lokale DB bereit")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[DesktopApp] DB-Warnung: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
Window(
|
Window(onCloseRequest = ::exitApplication, title = "Meldestelle Desktop") {
|
||||||
onCloseRequest = ::exitApplication,
|
|
||||||
title = "Meldestelle",
|
|
||||||
state = WindowState(width = 1600.dp, height = 900.dp),
|
|
||||||
) {
|
|
||||||
DesktopApp()
|
DesktopApp()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-31
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Send
|
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 androidx.compose.ui.unit.sp
|
||||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
import java.time.LocalTime
|
import at.mocode.frontend.shell.desktop.screens.chat.presentation.ChatMessageState
|
||||||
import java.time.format.DateTimeFormatter
|
import at.mocode.frontend.shell.desktop.screens.chat.presentation.ChatViewModel
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
data class ChatMessage(
|
|
||||||
val id: String,
|
|
||||||
val sender: String,
|
|
||||||
val text: String,
|
|
||||||
val time: String,
|
|
||||||
val isFromMe: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(
|
fun ChatScreen(
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit,
|
||||||
|
viewModel: ChatViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
var messageText by remember { mutableStateOf("") }
|
var messageText by remember { mutableStateOf("") }
|
||||||
val messages = remember { mutableStateListOf<ChatMessage>() }
|
val messages by viewModel.messages.collectAsState()
|
||||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
val peerCount by viewModel.peerCount.collectAsState()
|
||||||
|
val scrollState = rememberLazyListState()
|
||||||
|
|
||||||
// Mock initial messages
|
// Auto-scroll to bottom on new messages
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(messages.size) {
|
||||||
if (messages.isEmpty()) {
|
if (messages.isNotEmpty()) {
|
||||||
messages.add(ChatMessage("1", "Richter-Turm 1", "Startliste für Bewerb 5 ist fertig?", "10:45", false))
|
scrollState.animateScrollToItem(messages.size - 1)
|
||||||
messages.add(ChatMessage("2", "Meldestelle", "Ja, wird gerade gedruckt.", "10:46", true))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +56,9 @@ fun ChatScreen(
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"LAN-Kanal: aktiv (3 Teilnehmer)",
|
"LAN-Kanal: aktiv ($peerCount Teilnehmer verbunden)",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
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
|
// Chat Messages
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = scrollState,
|
||||||
modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = Dimens.SpacingM),
|
modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = Dimens.SpacingM),
|
||||||
contentPadding = PaddingValues(vertical = Dimens.SpacingM),
|
contentPadding = PaddingValues(vertical = Dimens.SpacingM),
|
||||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||||
) {
|
) {
|
||||||
items(messages) { msg ->
|
items(messages, key = { it.id }) { msg ->
|
||||||
ChatBubble(msg)
|
ChatBubble(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,18 +98,11 @@ fun ChatScreen(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (messageText.isNotBlank()) {
|
if (messageText.isNotBlank()) {
|
||||||
messages.add(
|
viewModel.sendMessage(messageText)
|
||||||
ChatMessage(
|
|
||||||
id = messages.size.toString(),
|
|
||||||
sender = "Meldestelle",
|
|
||||||
text = messageText,
|
|
||||||
time = LocalTime.now().format(timeFormatter),
|
|
||||||
isFromMe = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
messageText = ""
|
messageText = ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
enabled = messageText.isNotBlank(),
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
@@ -128,7 +117,7 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChatBubble(msg: ChatMessage) {
|
private fun ChatBubble(msg: ChatMessageState) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = if (msg.isFromMe) Alignment.End else Alignment.Start
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
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 Configuration
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Increased Kotlin Daemon Heap for JS Compilation
|
# Increased Kotlin Daemon Heap for JS Compilation + JDK 25 Warning Suppression
|
||||||
kotlin.daemon.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g
|
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.js.compiler.sourcemaps=false
|
||||||
|
|
||||||
# Kotlin Compiler Optimizations (Phase 5)
|
# Kotlin Compiler Optimizations
|
||||||
kotlin.incremental=true
|
kotlin.incremental=true
|
||||||
kotlin.incremental.multiplatform=true
|
kotlin.incremental.multiplatform=true
|
||||||
kotlin.incremental.js=true
|
kotlin.incremental.js=true
|
||||||
|
|
||||||
kotlin.caching.enabled=true
|
kotlin.caching.enabled=true
|
||||||
kotlin.compiler.execution.strategy=in-process
|
kotlin.compiler.execution.strategy=in-process
|
||||||
# kotlin.compiler.preciseCompilationResultsBackup=true
|
|
||||||
kotlin.stdlib.default.dependency=true
|
kotlin.stdlib.default.dependency=true
|
||||||
|
|
||||||
# Gradle Configuration
|
# Gradle Configuration
|
||||||
# Increased Gradle Daemon Heap
|
# Optimized for JDK 25: Added --add-opens and --enable-native-access for compiler tools
|
||||||
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.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=8
|
org.gradle.workers.max=12
|
||||||
org.gradle.vfs.watch=true
|
org.gradle.vfs.watch=true
|
||||||
|
# Configuration Cache (Enabled for performance with many modules)
|
||||||
# Configuration Cache optimieren - TEMPORÄR DEAKTIVIERT wegen JS-Test Serialisierungsproblemen
|
org.gradle.configuration-cache=true
|
||||||
org.gradle.configuration-cache=false
|
|
||||||
org.gradle.configuration-cache.problems=warn
|
org.gradle.configuration-cache.problems=warn
|
||||||
|
|
||||||
# Build Performance verbessern
|
# Build Performance
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
|
|
||||||
@@ -46,7 +43,7 @@ org.jetbrains.kotlin.wasm.check.wasm.binary.format=false
|
|||||||
kotlin.native.ignoreDisabledTargets=true
|
kotlin.native.ignoreDisabledTargets=true
|
||||||
idea.project.settings.delegate.build.run.actions.to.gradle=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
|
kotlin.js.yarn.ignoreScripts=false
|
||||||
org.jetbrains.kotlin.js.yarn.ignoreScripts=false
|
org.jetbrains.kotlin.js.yarn.ignoreScripts=false
|
||||||
kotlin.js.npm.ignoreScripts=false
|
kotlin.js.npm.ignoreScripts=false
|
||||||
@@ -56,31 +53,21 @@ org.jetbrains.kotlin.js.npm.ignoreScripts=false
|
|||||||
org.gradle.logging.level=lifecycle
|
org.gradle.logging.level=lifecycle
|
||||||
kotlin.build.report.single_file=false
|
kotlin.build.report.single_file=false
|
||||||
|
|
||||||
# Compose Experimental Features
|
# Compose Experimental
|
||||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||||
org.jetbrains.compose.experimental.wasm.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-download=true
|
||||||
org.gradle.java.installations.auto-detect=true
|
org.gradle.java.installations.auto-detect=true
|
||||||
|
|
||||||
# Development Environment Support
|
# Feature Toggles
|
||||||
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.
|
|
||||||
enableWasm=true
|
enableWasm=true
|
||||||
enableDesktop=true
|
enableDesktop=true
|
||||||
|
dev.port.offset=0
|
||||||
|
|
||||||
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
|
# Dokka V2
|
||||||
# See https://kotl.in/dokka-gradle-migration
|
|
||||||
# org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers
|
|
||||||
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
|
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
|
||||||
|
|
||||||
# Workaround for Gradle 9 / KMP "Plugin loaded multiple times" error in Docker/CI
|
# Gradle 9 Workaround
|
||||||
# This allows subprojects to re-declare plugins even if they are already on the classpath
|
|
||||||
kotlin.mpp.allowMultiplePluginDeclarations=true
|
kotlin.mpp.allowMultiplePluginDeclarations=true
|
||||||
|
|||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "==========================================="
|
||||||
|
echo "Meldestelle - Netzwerk-Optimierung (Firewall)"
|
||||||
|
echo "==========================================="
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Bitte mit sudo ausführen: sudo ./setup-firewall-linux.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ports:
|
||||||
|
# 8080 (P2P Sync), 8090 (Chat WS), 5353 (mDNS)
|
||||||
|
# 8500 (Consul UI - optional), 8600 (Consul DNS - optional)
|
||||||
|
|
||||||
|
open_ports_firewalld() {
|
||||||
|
echo "[Fedora/firewalld] Konfiguriere..."
|
||||||
|
firewall-cmd --permanent --add-port=8080/tcp
|
||||||
|
firewall-cmd --permanent --add-port=8090/tcp
|
||||||
|
firewall-cmd --permanent --add-service=mdns
|
||||||
|
# Optional: Consul Ports falls nötig
|
||||||
|
# firewall-cmd --permanent --add-port=8500/tcp
|
||||||
|
firewall-cmd --reload
|
||||||
|
echo "Fertig!"
|
||||||
|
}
|
||||||
|
|
||||||
|
open_ports_ufw() {
|
||||||
|
echo "[Ubuntu/ufw] Konfiguriere..."
|
||||||
|
ufw allow 8080/tcp comment 'Meldestelle Sync'
|
||||||
|
ufw allow 8090/tcp comment 'Meldestelle Chat'
|
||||||
|
ufw allow 5353/udp comment 'mDNS Discovery'
|
||||||
|
ufw reload
|
||||||
|
echo "Fertig!"
|
||||||
|
}
|
||||||
|
|
||||||
|
if command -v firewall-cmd &> /dev/null; then
|
||||||
|
open_ports_firewalld
|
||||||
|
elif command -v ufw &> /dev/null; then
|
||||||
|
open_ports_ufw
|
||||||
|
else
|
||||||
|
echo "Keine unterstützte Firewall (ufw/firewalld) gefunden."
|
||||||
|
echo "Bitte öffnen Sie manuell: 8080/tcp, 8090/tcp und 5353/udp."
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user