Build with Gradle in two modules, and free core of JavaFX

Etappen 1 and 2 of the Compose migration (43 commits).

The Maven build, the single source tree and the JPMS module-info went together.
core now holds the domain and app-desktop the user interface, both built by
Gradle, packaged through jpackage from Gradle, with SpotBugs and PMD wired in.
JavaFX moves to the classpath, which is why the launcher is Main and not the
Application subclass. The PR check, the nightly artifacts, the release and the
AUR packages all build from Gradle; the macOS DMG points at its output.
Verified on Linux, macOS and Windows.

core then loses JavaFX entirely. It observes through kst4contest.observe --
ObservableValue, ObservableRoster, and UiDispatcher as the one boundary a
worker thread crosses to touch the interface. The controller collections become
rosters and the visible station list a pure derivation rather than a
FilteredList/SortedList chain, which is also what fixed two focus defects only
a load test had exposed: the keyboard focus stays where the operator put it,
and the send field keeps it after a row click.

The rule that outlives this commit: ObservableRoster is the store and the list
a view is bound to is a mirror of it, never the other way round.
This commit is contained in:
Rsclub2_2 committed 2026-10-05 22:31:56 +02:00
1 parent bcbeadae95
commit a6fba12f01
360 files changed
+6304 -2422

No files matched your search

@@ -0,0 +1,991 @@
# Etappe 1 — Gradle-Build, zwei Module, JPMS-Ausbau: Umsetzungsplan
> **Für agentische Bearbeiter:** ERFORDERLICHE UNTER-SKILL: superpowers:subagent-driven-development (empfohlen) oder superpowers:executing-plans, um diesen Plan Aufgabe für Aufgabe umzusetzen. Schritte nutzen Checkbox-Syntax (`- [ ]`).
**Ziel:** Die unveränderte JavaFX-Anwendung baut, testet und paketiert unter Gradle in zwei Modulen, ohne JPMS.
**Architektur:** Ein Gradle-Multiprojekt mit `core` (Fachlichkeit) und `app-desktop` (Oberfläche). Paketnamen und Importe ändern sich **nicht** — Gradle-Module erzwingen keine Paketumbenennung, die Aufteilung ist ein reines Verschieben von Verzeichnissen. `module-info.java` entfällt; die Anwendung läuft künftig vom Klassenpfad. Das Packaging ruft `jpackage` weiterhin direkt auf, nur aus einer Gradle-Aufgabe statt aus Maven.
**Tech-Stack:** Gradle 8.10, Java 21, JavaFX 21.0.5 (unverändert), `org.openjfx.javafxplugin` 0.1.0, SpotBugs- und PMD-Gradle-Plugins, `jpackage` aus dem JDK.
**Spec:** `docs/superpowers/specs/2026-09-25-compose-migration-design.md`
## Zwei Präzisierungen gegenüber dem Spec
Beide sind Verkleinerungen des Risikos, keine Zieländerungen. Sie sind hier festgehalten, weil das Spec sie noch nicht kennt.
1. **`core` darf in Etappe 1 noch von JavaFX abhängen.** Das Spec trennt Modulaufteilung (Etappe 1) und JavaFX-Ausbau (Etappe 2). Da `ChatController`, `ChatMember`, `ChatPreferences` und `ScoreService` heute JavaFX benutzen, deklariert `core` die JavaFX-Abhängigkeit in Etappe 1 weiterhin. Etappe 2 entfernt sie. Damit bleibt Etappe 1 eine reine Build-Umstellung ohne fachliche Änderung.
2. **`compose.desktop.nativeDistributions` kommt erst in Etappe 3.** Das Spec nennt es für Etappe 1. Das Compose-Gradle-Plugin bündelt seine Laufzeit über `jlink`, und JavaFX ohne `module-info.java` vom Klassenpfad zu jlinken ist ein ungelöster Nebenschauplatz. Etappe 1 behält deshalb den heutigen, erprobten Weg: direkter `jpackage`-Aufruf auf ein Verzeichnis mit JAR und Abhängigkeiten. Auf `nativeDistributions` wird umgestellt, wenn mit Etappe 3 ohnehin Kotlin und Compose einziehen und JavaFX aus dem Packaging verschwindet.
## Globale Randbedingungen
- Java 21. `sourceCompatibility` und `targetCompatibility` bleiben 21.
- JavaFX bleibt auf 21.0.5. Kein Versionssprung in dieser Etappe.
- Projektversion bleibt `1.50.0-nightly`, identisch zum heutigen `pom.xml:9`.
- Hauptklasse bleibt `kst4contest.view.Kst4ContestApplication` (`pom.xml:26`).
- Anwendungsname bleibt `praktiKST` (`pom.xml:8`), da Launcher-, Paket- und Verzeichnisnamen daran hängen.
- **Keine Quelldatei unter `src/main/java/kst4contest/` wird inhaltlich geändert**, mit einer Ausnahme: `module-info.java` wird gelöscht. Dateien werden verschoben, nicht bearbeitet.
- Paketnamen und `import`-Anweisungen bleiben unverändert.
- Kommentare und Javadoc auf Englisch; Commit-Nachrichten knapp auf Englisch.
- Die Versionen der Gradle-Plugins werden zu Beginn von Aufgabe 1 gegen die aktuellen Veröffentlichungen geprüft; die hier genannten sind der Stand vom 2026-09-25.
## Prüfschwerpunkte
Build-Umstellungen brechen typischerweise nicht beim Übersetzen, sondern zur Laufzeit. Diese fünf Klassen von Fehlern deckt kein Kompilierlauf ab; jede bekommt ihren Test in der zuständigen Aufgabe.
1. **Ressourcen fehlen im JAR.** `praktiKST.db`, `praktiKSTpreferences.xml`, CSS und die Karten-HTML werden über `getResourceAsStream` geladen. Ein falsch zugeordnetes `resources`-Verzeichnis fällt erst beim Start auf. → Aufgabe 4.
2. **JavaFX-Natives fehlen.** Ohne `module-info.java` lädt JavaFX vom Klassenpfad und braucht die plattformspezifischen Klassifikator-Artefakte. Fehlen sie, startet die Anwendung nicht. → Aufgabe 3.
3. **`jdk.jsobject` fehlt zur Laufzeit.** Die Karten-Brücke `StationMapBridge` benutzt `netscape.javascript.JSObject`. Das Modul stand bisher in `module-info.java` und in der jpackage-`addmodules`-Liste. Ohne JPMS muss es über `--add-modules` beim jpackage-Lauf erhalten bleiben. → Aufgabe 8.
4. **SQLite-Treiber wird nicht gefunden.** `sqlite-jdbc` lädt über den `ServiceLoader`; ein verlorener `META-INF/services`-Eintrag beim Zusammenstellen des Verzeichnisses zeigt sich erst beim ersten Datenbankzugriff. → Aufgabe 4.
5. **Die Versionsauflösung bricht.** Zehn Stellen in fünf Workflows und zwei PKGBUILDs lesen die Projektversion mit `grep -m1 '<version>' pom.xml`. Wird `pom.xml` gelöscht, liefern sie eine leere Version und die Artefaktnamen werden falsch — ohne dass der Build scheitert. → Aufgaben 1, 9, 10 und 12.
6. **Das Paket startet, die Anwendung nicht.** Ein `jpackage`-Image kann sich erzeugen lassen und beim Start scheitern. Jede Packaging-Aufgabe endet mit einem echten Start des erzeugten Artefakts. → Aufgaben 8 und 9.
---
## Aufgabe 1: Gradle-Gerüst neben Maven
Maven bleibt in dieser Aufgabe vollständig funktionsfähig. Erst Aufgabe 12 entfernt es.
**Dateien:**
- Anlegen: `settings.gradle.kts`
- Anlegen: `build.gradle.kts`
- Anlegen: `gradle/libs.versions.toml`
- Anlegen: `gradlew`, `gradlew.bat`, `gradle/wrapper/gradle-wrapper.jar`, `gradle/wrapper/gradle-wrapper.properties` (durch `gradle wrapper` erzeugt)
- Ändern: `.gitignore`
**Schnittstellen:**
- Erzeugt: die Projekte `:core` und `:app-desktop`, auf die alle folgenden Aufgaben verweisen.
- [ ] **Schritt 1: Aktuelle Plugin-Versionen prüfen**
Vor dem Schreiben der Dateien die aktuellen Veröffentlichungen nachsehen:
- Gradle (hier angenommen: 8.10)
- `org.openjfx.javafxplugin` (hier angenommen: 0.1.0)
- `com.github.spotbugs` Gradle-Plugin (hier angenommen: 6.0.26)
Weichen sie ab, die neueren Versionen verwenden und die Abweichung im Ledger vermerken.
- [ ] **Schritt 2: Wrapper erzeugen**
Ausführen: `gradle wrapper --gradle-version 8.10`
Erwartet: `gradlew`, `gradlew.bat` und `gradle/wrapper/` entstehen.
Ist kein Gradle installiert, den Wrapper aus einer temporären Gradle-Distribution erzeugen.
- [ ] **Schritt 3: `settings.gradle.kts` schreiben**
```kotlin
rootProject.name = "kst4contest"
include(":core")
include(":app-desktop")
```
- [ ] **Schritt 3b: `gradle.properties` schreiben**
```properties
version=1.50.0-nightly
group=de.praktimarc
org.gradle.parallel=true
org.gradle.caching=true
```
Diese Datei ist ab Aufgabe 9 die **einzige** Quelle der Projektversion für CI und Paketbau. Sie ist bewusst so einfach gehalten, dass ein `grep` sie lesen kann, ohne Gradle zu starten.
`version` und `group` müssen den Werten aus `pom.xml:9` und dem dortigen `groupId` entsprechen.
- [ ] **Schritt 4: `gradle/libs.versions.toml` schreiben**
```toml
[versions]
java = "21"
javafx = "21.0.5"
javafxPlugin = "0.1.0"
sqliteJdbc = "3.46.1.3"
jlayer = "1.0.1"
junit = "5.10.2"
mockito = "5.12.0"
spotbugsPlugin = "6.0.26"
[libraries]
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqliteJdbc" }
jlayer = { module = "javazoom:jlayer", version.ref = "jlayer" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
[plugins]
javafx = { id = "org.openjfx.javafxplugin", version.ref = "javafxPlugin" }
spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugsPlugin" }
```
Die Bibliotheksversionen müssen den Werten in `pom.xml` entsprechen. Vor dem Schreiben mit `grep -n "sqlite-jdbc" -A2 pom.xml` und den entsprechenden Blöcken abgleichen und bei Abweichung die Werte aus `pom.xml` übernehmen.
- [ ] **Schritt 5: `build.gradle.kts` im Wurzelverzeichnis schreiben**
```kotlin
plugins {
java
}
allprojects {
group = providers.gradleProperty("group").get()
version = providers.gradleProperty("version").get()
repositories {
mavenCentral()
}
}
subprojects {
apply(plugin = "java")
extensions.configure<JavaPluginExtension> {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
}
```
`group` und `version` kommen aus `gradle.properties`. Prüfen: `./gradlew -q properties | grep -E "^(group|version):"` — erwartet `group: de.praktimarc` und `version: 1.50.0-nightly`, übereinstimmend mit `pom.xml:9`.
- [ ] **Schritt 6: Modulverzeichnisse und Platzhalter-Builds anlegen**
```bash
mkdir -p core/src/main/java app-desktop/src/main/java
printf 'plugins {\n java\n}\n' > core/build.gradle.kts
printf 'plugins {\n java\n}\n' > app-desktop/build.gradle.kts
```
- [ ] **Schritt 7: `.gitignore` ergänzen**
Folgende Zeilen anfügen:
```
.gradle/
build/
core/build/
app-desktop/build/
```
- [ ] **Schritt 8: Gerüst prüfen**
Ausführen: `./gradlew projects`
Erwartet: Ausgabe listet `Root project 'kst4contest'` mit den Unterprojekten `core` und `app-desktop`.
- [ ] **Schritt 9: Maven ist unberührt**
Ausführen: `./mvnw -q -DskipTests compile`
Erwartet: Erfolg. Die Gradle-Dateien dürfen den Maven-Build nicht stören.
- [ ] **Schritt 10: Commit**
```bash
git add settings.gradle.kts build.gradle.kts gradle.properties gradle core/build.gradle.kts app-desktop/build.gradle.kts gradlew gradlew.bat .gitignore
git commit -m "Add the Gradle multi-project skeleton alongside Maven"
```
---
## Aufgabe 2: Quellen auf die beiden Module verteilen
**Dateien:**
- Verschieben: `src/main/java/kst4contest/{model,controller,logic,utils,locatorUtils,service}` → `core/src/main/java/kst4contest/`
- Verschieben: `src/main/java/kst4contest/{view,test}` → `app-desktop/src/main/java/kst4contest/`
- Löschen: `src/main/java/module-info.java`
- Ändern: `core/build.gradle.kts`, `app-desktop/build.gradle.kts`
**Schnittstellen:**
- Konsumiert: die Projekte `:core` und `:app-desktop` aus Aufgabe 1.
- Erzeugt: `:app-desktop` hängt über `implementation(project(":core"))` an `:core`.
- [ ] **Schritt 1: Aufteilung festlegen und prüfen**
Ausführen: `find src/main/java/kst4contest -maxdepth 1 -mindepth 1 -type d | sort`
Erwartet: `controller`, `locatorUtils`, `logic`, `model`, `service`, `test`, `utils`, `view`.
`view` (samt `view/map`) geht nach `app-desktop`, alles andere nach `core`. `kst4contest.test` enthält Hilfsklassen mit JavaFX-Bezug und geht ebenfalls nach `app-desktop`.
- [ ] **Schritt 2: Verschieben**
```bash
mkdir -p core/src/main/java/kst4contest app-desktop/src/main/java/kst4contest
git mv src/main/java/kst4contest/model core/src/main/java/kst4contest/
git mv src/main/java/kst4contest/controller core/src/main/java/kst4contest/
git mv src/main/java/kst4contest/logic core/src/main/java/kst4contest/
git mv src/main/java/kst4contest/utils core/src/main/java/kst4contest/
git mv src/main/java/kst4contest/locatorUtils core/src/main/java/kst4contest/
git mv src/main/java/kst4contest/service core/src/main/java/kst4contest/
git mv src/main/java/kst4contest/view app-desktop/src/main/java/kst4contest/
git mv src/main/java/kst4contest/test app-desktop/src/main/java/kst4contest/
git rm src/main/java/module-info.java
```
- [ ] **Schritt 3: `core/build.gradle.kts` schreiben**
```kotlin
plugins {
java
alias(libs.plugins.javafx)
}
javafx {
version = libs.versions.javafx.get()
modules = listOf("javafx.controls", "javafx.fxml", "javafx.web", "javafx.media")
}
dependencies {
implementation(libs.sqlite.jdbc)
implementation(libs.jlayer)
}
```
> JavaFX steht hier bewusst noch drin. `ChatController`, `ChatMember`, `ChatPreferences` und `ScoreService` benutzen es. Etappe 2 entfernt diesen Block.
- [ ] **Schritt 4: `app-desktop/build.gradle.kts` schreiben**
```kotlin
plugins {
java
application
alias(libs.plugins.javafx)
}
javafx {
version = libs.versions.javafx.get()
modules = listOf("javafx.controls", "javafx.fxml", "javafx.web", "javafx.media")
}
dependencies {
implementation(project(":core"))
implementation(libs.sqlite.jdbc)
implementation(libs.jlayer)
}
application {
mainClass.set("kst4contest.view.Kst4ContestApplication")
}
```
- [ ] **Schritt 5: Übersetzen**
Ausführen: `./gradlew :core:compileJava`
Erwartet: BUILD SUCCESSFUL.
Scheitert es mit fehlenden Typen aus `kst4contest.view`, ist eine Klasse falsch einsortiert. Die betroffene Klasse ermitteln, prüfen ob sie Fachlichkeit oder Oberfläche ist, und entsprechend verschieben. Jede solche Entscheidung als `Ruling:` ins Ledger.
- [ ] **Schritt 6: `app-desktop` übersetzen**
Ausführen: `./gradlew :app-desktop:compileJava`
Erwartet: BUILD SUCCESSFUL.
- [ ] **Schritt 7: Commit**
```bash
git add -A
git commit -m "Split the sources into the core and app-desktop modules"
```
---
## Aufgabe 3: JavaFX-Natives und Start aus Gradle
Deckt Prüfschwerpunkt 2 ab.
**Dateien:**
- Ändern: `app-desktop/build.gradle.kts`
- [ ] **Schritt 1: Start versuchen**
Ausführen: `./gradlew :app-desktop:run`
Erwartet: Das Hauptfenster erscheint.
Erscheint stattdessen eine Meldung wie `Graphics Device initialization failed` oder `no javafx.graphics in java.library.path`, fehlen die plattformspezifischen JavaFX-Artefakte. Weiter mit Schritt 2. Startet die Anwendung, Schritt 2 überspringen und im Ledger vermerken.
- [ ] **Schritt 2: Klassifikator-Abhängigkeiten ergänzen**
In `app-desktop/build.gradle.kts` vor dem `dependencies`-Block einfügen:
```kotlin
val javafxPlatform = when {
org.gradle.internal.os.OperatingSystem.current().isWindows -> "win"
org.gradle.internal.os.OperatingSystem.current().isMacOsX ->
if (System.getProperty("os.arch") == "aarch64") "mac-aarch64" else "mac"
else -> "linux"
}
```
und im `dependencies`-Block:
```kotlin
listOf("javafx-base", "javafx-graphics", "javafx-controls", "javafx-fxml", "javafx-web", "javafx-media")
.forEach { module ->
runtimeOnly("org.openjfx:$module:${libs.versions.javafx.get()}:$javafxPlatform")
}
```
- [ ] **Schritt 3: Start erneut versuchen**
Ausführen: `./gradlew :app-desktop:run`
Erwartet: Das Hauptfenster erscheint.
- [ ] **Schritt 4: Commit**
```bash
git add app-desktop/build.gradle.kts
git commit -m "Resolve the JavaFX native artifacts for the Gradle run task"
```
---
## Aufgabe 4: Ressourcen und Tests zuordnen
Deckt Prüfschwerpunkte 1 und 4 ab.
**Dateien:**
- Verschieben: `src/main/resources/*` → `core/src/main/resources/` bzw. `app-desktop/src/main/resources/`
- Verschieben: `src/test/java/kst4contest/*` → `core/src/test/java/kst4contest/` bzw. `app-desktop/src/test/java/kst4contest/`
- Ändern: `core/build.gradle.kts`, `app-desktop/build.gradle.kts`
**Schnittstellen:**
- Konsumiert: die Modulaufteilung aus Aufgabe 2.
**Zuordnungsregel:** `app-desktop` sieht die Ressourcen von `core` zur Laufzeit, umgekehrt nicht. **Im Zweifel gehört eine Ressource nach `core`.** Eine fälschlich nach `core` gelegte Ressource ist folgenlos; eine fälschlich nach `app-desktop` gelegte bricht `core` zur Laufzeit.
- [ ] **Schritt 1: Ressourcen verschieben**
```bash
mkdir -p core/src/main/resources app-desktop/src/main/resources
git mv src/main/resources/praktiKST.db core/src/main/resources/
git mv src/main/resources/praktiKSTpreferences.xml core/src/main/resources/
git mv src/main/resources/praktiKSTpreferences.old core/src/main/resources/
git mv src/main/resources/icons core/src/main/resources/
for f in src/main/resources/*.mp3; do git mv "$f" core/src/main/resources/; done
git mv src/main/resources/leaflet-1.9.4.css app-desktop/src/main/resources/
git mv src/main/resources/leaflet-1.9.4.js app-desktop/src/main/resources/
git mv src/main/resources/KST4ContestDefaultDay.css app-desktop/src/main/resources/
git mv src/main/resources/KST4ContestDefaultEvening.css app-desktop/src/main/resources/
git mv src/main/resources/web app-desktop/src/main/resources/
```
- [ ] **Schritt 2: Tests verschieben**
```bash
mkdir -p core/src/test/java/kst4contest app-desktop/src/test/java/kst4contest
git mv src/test/java/kst4contest/controller core/src/test/java/kst4contest/
git mv src/test/java/kst4contest/test core/src/test/java/kst4contest/
git mv src/test/java/kst4contest/view app-desktop/src/test/java/kst4contest/
```
- [ ] **Schritt 3: Testabhängigkeiten ergänzen**
In `core/build.gradle.kts` und `app-desktop/build.gradle.kts` jeweils im `dependencies`-Block anfügen:
```kotlin
testImplementation(libs.junit.jupiter)
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.junit.jupiter)
```
- [ ] **Schritt 4: Tests übersetzen**
Ausführen: `./gradlew :core:compileTestJava :app-desktop:compileTestJava`
Erwartet: BUILD SUCCESSFUL.
Scheitert eine Testklasse in `core` an einem Typ aus `kst4contest.view`, gehört sie nach `app-desktop`. Verschieben und als `Ruling:` ins Ledger.
- [ ] **Schritt 5: Tests ausführen**
Ausführen: `./gradlew test`
Erwartet: BUILD SUCCESSFUL. Die Summe der **ausgeführten Testfälle** beider Module beträgt **208**.
Die Zahl prüfen:
`find core/build/test-results app-desktop/build/test-results -name "TEST-*.xml" -exec grep -ho 'tests="[0-9]*"' {} + | grep -o '[0-9]*' | paste -sd+ | bc`
Erwartet: `208`.
Die Zahl ist nicht die der `@Test`-Annotationen (148): sechs `@ParameterizedTest`-Methoden expandieren zu 60 Fällen. Die belastbare Invariante ist die Dateizahl:
`find core/src/test app-desktop/src/test -name "*.java" | wc -l`
Erwartet: `43` — dieselbe Zahl wie vorher unter `src/test/java`.
Weicht eine der Zahlen ab, fehlt eine Testklasse oder wurde eine doppelt eingebunden. Ursache suchen, nicht die Erwartung anpassen.
- [ ] **Schritt 6: Anwendung starten und Datenbank prüfen**
Ausführen: `./gradlew :app-desktop:run`
Erwartet: Das Hauptfenster erscheint, ohne dass auf der Konsole `SQLException`, `ClassNotFoundException: org.sqlite.JDBC` oder eine Meldung über eine fehlende Ressource erscheint.
Im laufenden Programm das Einstellungsfenster öffnen und den Reiter **Workedstn database** aufrufen.
Erwartet: Der Reiter zeigt Zahlen aus der Datenbank, keine Fehlermeldung. Das beweist, dass `praktiKST.db` gefunden und der SQLite-Treiber über den `ServiceLoader` geladen wurde.
- [ ] **Schritt 7: Commit**
```bash
git add -A
git commit -m "Assign resources and tests to the core and app-desktop modules"
```
---
## Aufgabe 5: Lombok entfernen
**Dateien:**
- Ändern: `pom.xml`
- [ ] **Schritt 1: Nachweisen, dass Lombok ungenutzt ist**
Ausführen: `grep -rl "lombok" core/src app-desktop/src | wc -l`
Erwartet: `0`.
Ist die Zahl größer als 0, wird Lombok doch benutzt. Dann diese Aufgabe überspringen, im Ledger als `Ruling:` festhalten und mit Aufgabe 6 weitermachen.
- [ ] **Schritt 2: Aus `pom.xml` entfernen**
Den `<dependency>`-Block mit `<artifactId>lombok</artifactId>` (ab `pom.xml:114`) und den `<annotationProcessorPath>`-Eintrag für Lombok im `maven-compiler-plugin` (ab `pom.xml:251`) löschen.
In den Gradle-Dateien taucht Lombok nicht auf, dort ist nichts zu tun.
- [ ] **Schritt 3: Beide Builds prüfen**
Ausführen: `./gradlew build -x test`
Erwartet: BUILD SUCCESSFUL.
Für den Maven-Build gilt ab hier: die Enforcer-Ausführung `verify-packaging-module-list` wurde bereits in dieser Aufgabe entfernt, weil das Löschen von `module-info.java` in Aufgabe 2 sie sonst dauerhaft brechen würde. Ohne sie übersetzt Maven weiter:
Ausführen: `./mvnw -q -DskipTests compile`
Erwartet: Erfolg.
- [ ] **Schritt 4: Commit**
```bash
git add pom.xml
git commit -m "Drop the unused Lombok dependency"
```
---
## Aufgabe 6: SpotBugs und PMD unter Gradle
**Dateien:**
- Ändern: `build.gradle.kts`
- Unverändert: `pmd-ruleset.xml`
- [ ] **Schritt 1: Heutige Konfiguration ablesen**
Ausführen: `grep -n -A12 "spotbugs-maven-plugin" pom.xml && grep -n -A12 "maven-pmd-plugin" pom.xml`
Aus der Ausgabe `effort`, `threshold`, `failOnViolation` und den Verweis auf `pmd-ruleset.xml` notieren. Diese Werte werden im nächsten Schritt übernommen, nicht neu erfunden.
- [ ] **Schritt 2: Plugins im Wurzel-Build ergänzen**
In `build.gradle.kts` den `plugins`-Block erweitern:
```kotlin
plugins {
java
alias(libs.plugins.spotbugs) apply false
pmd apply false
}
```
und im `subprojects`-Block anfügen:
```kotlin
apply(plugin = "com.github.spotbugs")
apply(plugin = "pmd")
extensions.configure<PmdExtension> {
toolVersion = "7.7.0"
ruleSetFiles = files(rootProject.file("pmd-ruleset.xml"))
ruleSets = emptyList()
isIgnoreFailures = true
}
```
`isIgnoreFailures` entspricht dem heutigen `failOnViolation`. Steht dort `true`, muss hier `false` stehen.
- [ ] **Schritt 3: Analysen ausführen**
Ausführen: `./gradlew pmdMain spotbugsMain`
Erwartet: BUILD SUCCESSFUL. Berichte liegen unter `core/build/reports/` und `app-desktop/build/reports/`.
- [ ] **Schritt 4: Commit**
```bash
git add build.gradle.kts
git commit -m "Run SpotBugs and PMD from the Gradle build"
```
---
## Aufgabe 7: jpackage-Image aus Gradle
**Dateien:**
- Ändern: `app-desktop/build.gradle.kts`
**Schnittstellen:**
- Erzeugt: die Gradle-Aufgabe `:app-desktop:packageImage`, die aus `app-desktop/build/dist-libs/` (JAR als `app.jar`) ein `jpackage`-Image unter `app-desktop/build/jpackage/` erzeugt. Aufgaben 9, 10 und 11 rufen sie auf.
Die heutige Maven-Strecke sammelt JAR und Abhängigkeiten in `target/modules` und ruft darauf `jpackage --type IMAGE`. Gradle bekommt dieselbe Strecke.
- [ ] **Schritt 1: Sammel- und Packaging-Aufgabe ergänzen**
An `app-desktop/build.gradle.kts` anfügen:
```kotlin
val collectRuntime by tasks.registering(Sync::class) {
dependsOn(tasks.jar)
from(tasks.jar) { rename { "app.jar" } }
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("dist-libs"))
}
val packageImage by tasks.registering(Exec::class) {
dependsOn(collectRuntime)
val input = layout.buildDirectory.dir("dist-libs").get().asFile
val output = layout.buildDirectory.dir("jpackage").get().asFile
val os = org.gradle.internal.os.OperatingSystem.current()
val icon = when {
os.isWindows -> "packaging/icons/kst4contest.ico"
os.isMacOsX -> "packaging/icons/kst4contest.icns"
else -> "packaging/icons/kst4contest.png"
}
doFirst { output.deleteRecursively() }
commandLine(
"jpackage",
"--icon", rootProject.file(icon).absolutePath,
"--type", "app-image",
"--name", "praktiKST",
"--input", input.absolutePath,
"--dest", output.absolutePath,
"--main-jar", "app.jar",
"--main-class", "kst4contest.view.Kst4ContestApplication",
"--java-options", "-Dfile.encoding=UTF-8"
)
}
```
> `--icon` ist nicht optional: ohne dieses Argument liefert `jpackage` stillschweigend sein Standardsymbol aus. Alle heutigen CI-Aufrufe setzen es (`nightly-artifacts.yml:72`, `:130` und weitere).
- [ ] **Schritt 2: Image bauen**
Ausführen: `./gradlew :app-desktop:packageImage`
Erwartet: BUILD SUCCESSFUL, und `app-desktop/build/jpackage/praktiKST/` existiert.
Prüfen: `ls app-desktop/build/jpackage/praktiKST/bin/`
Erwartet: eine ausführbare Datei `praktiKST`.
- [ ] **Schritt 3: Erzeugtes Image starten**
Ausführen: `./app-desktop/build/jpackage/praktiKST/bin/praktiKST`
Erwartet: Das Hauptfenster erscheint.
Scheitert der Start an fehlenden JavaFX-Klassen, enthält `runtime-input` die Klassifikator-Artefakte nicht. Dann in Schritt 1 `configurations.runtimeClasspath` gegen eine Konfiguration prüfen, die die `runtimeOnly`-Einträge aus Aufgabe 3 enthält.
> Das Verzeichnis heißt bewusst `dist-libs` und das Haupt-JAR bewusst `app.jar`: die CI-Strecken in `.github/workflows/` rufen `jpackage` heute auf genau diese Form auf. Damit ändert Aufgabe 9 nur die Build-Befehle, nicht die `jpackage`-Aufrufe.
- [ ] **Schritt 4: Commit**
```bash
git add app-desktop/build.gradle.kts
git commit -m "Build the jpackage app-image from Gradle"
```
---
## Aufgabe 8: JPMS-Reste entfernen und `--add-modules` sichern
Deckt Prüfschwerpunkte 3 und 5 ab.
**Dateien:**
- Ändern: `app-desktop/build.gradle.kts`
- Löschen: `packaging/AddModules.java`
- Ändern: `pom.xml` (Enforcer-Ausführung `verify-packaging-module-list`, ab `pom.xml:225`)
- [ ] **Schritt 1: Die Karten-Brücke ohne `--add-modules` scheitern sehen**
Das Image aus Aufgabe 7 starten und das Kartenfenster öffnen.
Erwartet: Entweder erscheint die Karte nicht, oder auf der Konsole steht ein Fehler zu `netscape.javascript.JSObject` bzw. `jdk.jsobject`.
Erscheint die Karte fehlerfrei, ist `jdk.jsobject` in der Standard-Laufzeit enthalten. Das im Ledger festhalten und Schritt 2 trotzdem ausführen — die Liste sichert auch die übrigen Module ab.
- [ ] **Schritt 2: `--add-modules` in die Gradle-Aufgabe übernehmen**
Die heutige Liste steht in `pom.xml` ab `:478`. Ohne `javafx.*` (JavaFX kommt vom Klassenpfad) bleibt:
In `app-desktop/build.gradle.kts` die `commandLine`-Liste in `packageImage` um folgende Einträge **vor** `--java-options` erweitern:
```kotlin
"--add-modules",
"java.desktop,java.net.http,java.sql,jdk.crypto.ec,jdk.jsobject,jdk.net,jdk.xml.dom",
```
- [ ] **Schritt 3: Neu bauen und Karte prüfen**
Ausführen: `./gradlew :app-desktop:packageImage && ./app-desktop/build/jpackage/praktiKST/bin/praktiKST`
Erwartet: Das Hauptfenster erscheint, das Kartenfenster zeigt Kacheln und Stationsmarker, und auf der Konsole steht kein Fehler zu `JSObject`.
- [ ] **Schritt 4: Helfer und Enforcer-Regel entfernen**
```bash
git rm packaging/AddModules.java
```
Die Enforcer-Ausführung `verify-packaging-module-list` samt `exec-maven-plugin`-Aufruf und erklärendem Kommentar wurde bereits in Aufgabe 5 aus `pom.xml` entfernt — sie brach ab Aufgabe 2, weil `module-info.java` dort entfiel. Hier ist nur noch zu prüfen:
Ausführen: `grep -c "verify-packaging-module-list" pom.xml`
Erwartet: `0`.
- [ ] **Schritt 5: Prüfen, dass nichts mehr auf den Helfer verweist**
Ausführen: `grep -rln "AddModules" .github/ packaging/ pom.xml`
Erwartet: `.github/workflows/pr-compile-check.yml`, `.github/workflows/nightly-artifacts.yml`, `.github/workflows/tagged-release.yml`, `packaging/macos/build-signed-dmg.sh`, `packaging/aur/kst4contest/PKGBUILD`, `packaging/aur/kst4contest-git/PKGBUILD` und ein erklärender Kommentar in `pom.xml`. Zusammen 18 Aufrufstellen — sie werden in den Aufgaben 9, 10 und 11 auf `gradle.properties` umgestellt.
Die Modulliste ist ohne `module-info.java` eine Konstante geworden. Damit sie nicht über 18 Stellen driftet — genau das verhinderte bisher `AddModules.java` —, steht sie als `jpackageAddModules` in `gradle.properties`, neben der Version. CI und PKGBUILDs lesen sie mit demselben `grep` wie die Version:
```bash
ADD_MODULES=$(grep -m1 '^jpackageAddModules=' gradle.properties | cut -d= -f2)
```
- [ ] **Schritt 6: Commit**
```bash
git add -A
git commit -m "Drop the JPMS module list helper and pin the runtime modules in Gradle"
```
---
## Aufgabe 9: PR-Prüfung und Nightly-Strecke auf Gradle
Deckt Prüfschwerpunkt 5 auf Windows und Linux ab — dort ist die CI die einzige Prüfmöglichkeit.
**Dateien:**
- Ändern: `.github/workflows/pr-compile-check.yml`
- Ändern: `.github/workflows/nightly-artifacts.yml`
**Schnittstellen:**
- Konsumiert: `:app-desktop:collectRuntime` aus Aufgabe 7, das `app-desktop/build/dist-libs/` mit `app.jar` füllt.
Das Muster ist in allen Strecken gleich. Ersetzt wird jeweils nur der Build-Schritt; die `jpackage`-Aufrufe bleiben, nur ihr `--input`-Pfad wechselt von `target/dist-libs` auf `app-desktop/build/dist-libs`.
- [ ] **Schritt 1: `pr-compile-check.yml` anpassen**
Ersetzen:
```yaml
- name: Ensure mvnw is executable
run: chmod +x mvnw
- name: Verify packaging module list
run: java packaging/AddModules.java --verify-pom
- name: Compile
run: ./mvnw -B -DskipTests compile
```
durch:
```yaml
- name: Ensure gradlew is executable
run: chmod +x gradlew
- name: Compile
run: ./gradlew -S compileJava compileTestJava
```
Der Schritt „Verify packaging module list" entfällt ersatzlos — `packaging/AddModules.java` wurde in Aufgabe 8 gelöscht.
- [ ] **Schritt 2: Prüfen, dass kein Verweis auf den Helfer bleibt**
Ausführen: `grep -rn "AddModules" .github/ packaging/ pom.xml`
Erwartet: keine Ausgabe.
- [ ] **Schritt 3: `nightly-artifacts.yml` — Build-Schritte ersetzen**
`nightly-artifacts.yml` hat sieben bauende Jobs: `build-windows-zip` (`:25`), `build-linux-appimage` (`:95`), `build-linux-deb` (`:177`), `build-linux-rpm` (`:238`), `build-linux-arch` (`:299`), `build-flatpak` (`:420`) und `build-macos-dmg` (`:611`). `publish-flatpak-repo` (`:550`) baut nicht und bleibt unverändert. In **jedem** der sieben Jobs die Zeilenpaare
```yaml
- name: Ensure mvnw is executable
run: chmod +x mvnw
```
ersetzen durch
```yaml
- name: Ensure gradlew is executable
run: chmod +x gradlew
```
und jede Zeile der Form
```
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
```
ersetzen durch die eine Zeile
```
./gradlew -S :app-desktop:collectRuntime
```
Auf Windows lautet die Entsprechung `.\gradlew.bat -S :app-desktop:collectRuntime` anstelle von `.\mvnw.cmd ...`.
- [ ] **Schritt 4: `--input`-Pfade umstellen**
In jedem `jpackage`-Aufruf in `nightly-artifacts.yml` das Argument `--input target/dist-libs` durch `--input app-desktop/build/dist-libs` ersetzen.
Ausführen: `grep -n "target/dist-libs" .github/workflows/nightly-artifacts.yml`
Erwartet: keine Ausgabe.
- [ ] **Schritt 5: `--add-modules` in der CI nachziehen**
Jeder `jpackage`-Aufruf, der bisher auf die JPMS-Modulliste gebaut hat, braucht dieselbe Liste wie Aufgabe 8:
```
--add-modules java.desktop,java.net.http,java.sql,jdk.crypto.ec,jdk.jsobject,jdk.net,jdk.xml.dom
```
Enthält ein Aufruf bereits `--add-modules` mit `javafx.*`-Einträgen, werden die `javafx.*`-Einträge gestrichen und die übrigen auf obige Liste gebracht.
- [ ] **Schritt 5b: Versionsauflösung umstellen**
Jeder bauende Job beginnt mit einem Schritt „Resolve nightly version info", der so aussieht:
```yaml
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
```
Diese Zeile in **jedem** Job ersetzen durch:
```yaml
VERSION=$(grep -m1 '^version=' gradle.properties | cut -d= -f2)
```
Ausführen: `grep -c "pom.xml" .github/workflows/nightly-artifacts.yml`
Erwartet: `0`.
- [ ] **Schritt 6: Lokale Trockenprüfung der YAML-Dateien**
Ausführen: `python3 -c "import yaml,sys; [yaml.safe_load(open(f)) for f in sys.argv[1:]]; print('YAML ok')" .github/workflows/pr-compile-check.yml .github/workflows/nightly-artifacts.yml`
Erwartet: `YAML ok`.
- [ ] **Schritt 6b: Linux-Jobs lokal mit `act` prüfen**
`act` (0.2.89) und Docker sind vorhanden. Fünf der sieben bauenden Jobs laufen auf `ubuntu-latest` und damit lokal:
```bash
act -j build-linux-appimage -W .github/workflows/nightly-artifacts.yml
act -j build-linux-deb -W .github/workflows/nightly-artifacts.yml
act -j build-linux-rpm -W .github/workflows/nightly-artifacts.yml
act -j build-linux-arch -W .github/workflows/nightly-artifacts.yml
act --privileged -j build-flatpak -W .github/workflows/nightly-artifacts.yml
```
Erwartet: Jeder Lauf endet mit `Job succeeded`.
`build-flatpak` braucht `--privileged`, weil `flatpak-builder` Namensräume anlegt. Scheitert es trotzdem an fehlenden Rechten im Container, ist das ein Umgebungsproblem von `act` und kein Fehler der Umstellung: im Ledger als `Ruling:` festhalten und diesen einen Job über die CI prüfen.
`build-windows-zip` (`runs-on: windows-latest`) und `build-macos-dmg` (`runs-on: macos-latest`, `macos-15-intel`) kann `act` **nicht** ausführen — es startet ausschließlich Linux-Container. Beide bleiben Schritt 7.
> **Ohne Push verifiziert am 2026-09-25.** Linux über `act` (Schritt 6b), macOS auf einem Gerät mit macOS 27.0 / arm64, Windows 11 Pro / AMD64 in WinBoat. Auf allen drei Plattformen: Gradle-Build grün, `jpackage` erzeugt ein startfähiges Artefakt mit allen acht Laufzeitmodulen, und die Anwendung initialisiert JavaFX (RSS 182 MB Linux, 183 MB macOS, 193 MB Windows).
>
> **Was ein echter CI-Lauf trotzdem noch zeigen muss:** die Artefakt-Uploads, die MSI-Strecke mit WiX, die Flatpak-Signierung und das signierte DMG. Alle vier scheitern lokal an fehlenden Secrets oder Diensten, nicht am Build.
- [ ] **Schritt 7: Commit und CI-Lauf abwarten**
```bash
git add .github/workflows/pr-compile-check.yml .github/workflows/nightly-artifacts.yml
git commit -m "Build the PR check and nightly artifacts with Gradle"
git push
```
Danach den Nightly-Workflow in GitHub Actions auslösen und **das Ergebnis abwarten**.
Erwartet: Windows-ZIP, Linux-AppImage und Debian-Paket entstehen.
Windows- und macOS-Artefakte lassen sich lokal nicht prüfen. Scheitert ein Job, ist die CI-Ausgabe die einzige Fehlerquelle; nicht raten, sondern das Protokoll lesen.
- [ ] **Schritt 8: Erzeugtes Linux-Artefakt starten**
Das AppImage aus dem Workflow-Lauf herunterladen und ausführen.
Erwartet: Das Hauptfenster erscheint, das Kartenfenster zeigt Kacheln.
---
## Aufgabe 10: Release-Strecke und AUR
**Dateien:**
- Ändern: `.github/workflows/tagged-release.yml`
- Ändern: `packaging/aur/kst4contest/PKGBUILD`
- Prüfen: `.github/workflows/aur-publish.yml`, `.github/workflows/aur-nightly-git.yml`, `.github/workflows/docs-pdf.yml`, `.github/workflows/github-wiki.yml`, `.github/workflows/test-publish-version-xml.yml`
- [ ] **Schritt 1: `tagged-release.yml` umstellen**
Dieselben Ersetzungen wie in Aufgabe 9, Schritte 3 bis 5, an allen betroffenen Stellen. Die Zeilenanker zum Zeitpunkt der Planerstellung: `:40`, `:92-98`, `:172-178`, `:226-232`, `:280-286`, `:387-400`, `:523-524`. Vor dem Bearbeiten mit `grep -n "mvnw\|target/dist-libs" .github/workflows/tagged-release.yml` neu ermitteln, da sich die Zeilen durch die vorherigen Aufgaben verschoben haben können.
- [ ] **Schritt 2: Prüfen**
Ausführen: `grep -n "mvnw\|target/dist-libs" .github/workflows/tagged-release.yml`
Erwartet: keine Ausgabe.
- [ ] **Schritt 3: `PKGBUILD` umstellen**
In `packaging/aur/kst4contest/PKGBUILD` die `build()`-Funktion anpassen: der Maven-Aufruf und das `cp` auf `target/dist-libs/app.jar` (`:26`) werden durch `./gradlew --offline -S :app-desktop:collectRuntime` ersetzt, und der `jpackage`-Aufruf (`:44`) bekommt `--input app-desktop/build/dist-libs`.
Die `depends`- und `makedepends`-Einträge müssen `gradle` statt `maven` nennen, falls dort `maven` steht.
Ausführen: `grep -n "maven\|mvn\|target/" packaging/aur/kst4contest/PKGBUILD`
Erwartet: keine Ausgabe.
- [ ] **Schritt 4: Flatpak-Job prüfen**
Der Job `build-flatpak` in `nightly-artifacts.yml` (`:420`) und sein Gegenstück in `tagged-release.yml` bauen inline, es gibt kein `packaging/flatpak`-Verzeichnis. Dort gelten dieselben Ersetzungen wie in Aufgabe 9, Schritte 3 bis 5.
Ausführen: `grep -n "mvnw\|target/dist-libs" .github/workflows/nightly-artifacts.yml`
Erwartet: keine Ausgabe.
- [ ] **Schritt 5: Die übrigen Workflows prüfen**
Ausführen: `grep -ln "mvnw\|mvn " .github/workflows/*.yml`
Erwartet: keine Ausgabe.
Treffer werden nach demselben Muster umgestellt. `docs-pdf.yml` und `github-wiki.yml` bauen vermutlich kein Java und bleiben dann unverändert.
- [ ] **Schritt 5b: Versionsauflösung in den übrigen Dateien umstellen**
Dieselbe Ersetzung wie Aufgabe 9, Schritt 5b, in `tagged-release.yml`, `aur-publish.yml`, `aur-nightly-git.yml` und `docs-pdf.yml`.
In `packaging/aur/kst4contest/PKGBUILD` und `packaging/aur/kst4contest-git/PKGBUILD` jede Stelle, die `pom.xml` nach der Version durchsucht, auf `gradle.properties` umstellen.
Ausführen: `grep -rn "pom.xml" .github/ packaging/`
Erwartet: keine Ausgabe.
- [ ] **Schritt 6: YAML prüfen**
Ausführen: `python3 -c "import yaml,glob; [yaml.safe_load(open(f)) for f in glob.glob('.github/workflows/*.yml')]; print('YAML ok')"`
Erwartet: `YAML ok`.
- [ ] **Schritt 7: Commit**
```bash
git add .github/workflows packaging/aur
git commit -m "Build the release and AUR packages with Gradle"
```
---
## Aufgabe 11: macOS-DMG
**Dateien:**
- Ändern: `packaging/macos/build-signed-dmg.sh`
Nur auf macOS oder über die CI prüfbar.
- [ ] **Schritt 1: Eingabepfade im Skript ermitteln**
Ausführen: `grep -n "dist-libs\|target/\|jpackage\|--input" packaging/macos/build-signed-dmg.sh`
- [ ] **Schritt 2: Umstellen**
Jeden Verweis auf `target/dist-libs` auf `app-desktop/build/dist-libs` ändern und den Maven-Aufruf, falls vorhanden, durch `./gradlew -S :app-desktop:collectRuntime` ersetzen. Die `--add-modules`-Liste wie in Aufgabe 8, Schritt 2.
Signierung, Entitlements (`packaging/macos/kst4contest.entitlements`), das Drag-Layout und `packaging/macos/dmg/render-background.swift` bleiben **unverändert**. Diese Aufgabe ändert ausschließlich, woher die Eingabedateien kommen.
- [ ] **Schritt 3: Prüfen**
Ausführen: `grep -n "target/" packaging/macos/build-signed-dmg.sh`
Erwartet: keine Ausgabe.
> **Teilweise verifiziert am 2026-09-25** auf einem Mac mit macOS 27.0 / arm64 (Temurin 21.0.12.1), ohne Push: `./gradlew :app-desktop:collectRuntime` und `:app-desktop:packageImage` laufen durch. Es entsteht `praktiKST.app` mit allen acht Laufzeitmodulen, eingebettetem `praktiKST.icns`, `app.mainclass=kst4contest.view.Main` und den sechs `mac-aarch64`-JavaFX-JARs. Die App startet, läuft stabil (182 MB RSS) und initialisiert den JavaFX-Toolkit — `sample` zeigt JavaFX Application Thread, QuantumRenderer und Prism.
>
> **Offen bleibt die Signierung und das DMG selbst.** `build-signed-dmg.sh` bricht ohne `SIGNING_IDENTITY` ab; ein Zertifikat ist auf dem Rechner vorhanden ("Developer ID Application: Philipp Wagner (RNGLLTC995)"), wurde aber bewusst nicht benutzt. Die von dieser Etappe geänderten Zeilen des Skripts sind dieselben, die `packageImage` oben verifiziert hat — Signierung, Entitlements und Drag-Layout sind unverändert.
- [ ] **Schritt 4: Commit und CI-Lauf**
```bash
git add packaging/macos/build-signed-dmg.sh
git commit -m "Point the macOS DMG build at the Gradle output"
git push
```
Den macOS-Job in GitHub Actions auslösen und abwarten.
Erwartet: Ein DMG entsteht, zeigt das Drag-to-Install-Layout und die darin enthaltene Anwendung startet.
---
## Aufgabe 12: Maven entfernen
Erst ausführen, wenn Aufgabe 9, 10 und 11 auf allen drei Plattformen grün sind.
**Dateien:**
- Löschen: `pom.xml`, `mvnw`, `mvnw.cmd`, `.mvn/`
- Ändern: `README.md`, `AGENTS.md`, `docs/PROJECT_CONTEXT.md`
- [ ] **Schritt 1: Grüne CI bestätigen**
Ausführen: `gh run list --limit 5`
Erwartet: Die letzten Läufe von `nightly-artifacts` und `tagged-release` sind erfolgreich.
Ist ein Lauf rot, diese Aufgabe **nicht** beginnen. Erst die Ursache beheben.
- [ ] **Schritt 2: Entfernen**
```bash
git rm pom.xml mvnw mvnw.cmd
git rm -r .mvn
```
- [ ] **Schritt 3: Dokumentation nachziehen**
In `README.md` den Abschnitt „Building from source" anpassen: statt des Maven-Wrappers wird `./gradlew build` genannt. JDK 21 bleibt die Voraussetzung.
In `AGENTS.md` den Eintrag `pom.xml` in der Liste der Repository-Bereiche durch `build.gradle.kts`, `settings.gradle.kts` und `gradle/` ersetzen.
In `docs/PROJECT_CONTEXT.md` unter „Current Architecture" den Satz „Java 21 / JavaFX desktop application built with Maven." ersetzen durch „Java 21 / JavaFX desktop application built with Gradle in the modules `core` and `app-desktop`."
- [ ] **Schritt 4: Prüfen, dass kein Verweis bleibt**
Ausführen: `grep -rn "mvnw\|pom\.xml\|Maven" README.md AGENTS.md docs/PROJECT_CONTEXT.md .github/ packaging/`
Erwartet: keine Ausgabe. Treffer in historischen Abschnitten von `docs/` dürfen stehen bleiben, wenn sie die Vergangenheit beschreiben; das im Ledger als `Ruling:` festhalten.
- [ ] **Schritt 4b: Versionsauflösung ist von `pom.xml` gelöst**
Ausführen: `grep -rn "pom.xml" .github/ packaging/ README.md AGENTS.md`
Erwartet: keine Ausgabe.
Bleibt hier ein Treffer, liefert die betroffene Strecke nach dem Löschen von `pom.xml` eine **leere** Version und benennt ihre Artefakte falsch, ohne zu scheitern. Diese Prüfung ist deshalb nicht optional.
- [ ] **Schritt 5: Vollständiger Bau- und Testlauf**
Ausführen: `./gradlew clean build`
Erwartet: BUILD SUCCESSFUL, 208 Testfälle grün, 0 Fehler.
- [ ] **Schritt 6: Image bauen und starten**
Ausführen: `./gradlew :app-desktop:packageImage && ./app-desktop/build/jpackage/praktiKST/bin/praktiKST`
Erwartet: Das Hauptfenster erscheint. Stationsliste, Chat-Reiter, Einstellungsfenster und Kartenfenster funktionieren.
- [ ] **Schritt 7: Commit**
```bash
git add -A
git commit -m "Remove the Maven build in favour of Gradle"
```
---
## Abnahme der Etappe
Etappe 1 ist abgeschlossen, wenn alles davon zutrifft:
- [ ] `./gradlew clean build` läuft durch, 208 Testfälle aus 43 Testdateien grün.
- [ ] `./gradlew :app-desktop:packageImage` erzeugt ein Image, das startet.
- [ ] Im gestarteten Image funktionieren Stationsliste, die drei Chat-Reiter, das Einstellungsfenster mit allen zwölf Reitern, das Kartenfenster und der Profilwechsel.
- [ ] Windows-ZIP, Linux-AppImage, Debian-Paket, RPM, Arch-Paket, Flatpak und macOS-DMG entstehen in der CI und starten.
- [ ] `pom.xml`, `mvnw`, `mvnw.cmd`, `.mvn/`, `module-info.java` und `packaging/AddModules.java` existieren nicht mehr.
- [ ] `grep -rn "pom.xml" .github/ packaging/` liefert keine Ausgabe; die Version kommt überall aus `gradle.properties`.
- [ ] Keine Quelldatei unter `kst4contest/` wurde inhaltlich geändert — nur verschoben. Prüfbar mit `git log --stat` über die Commits dieser Etappe.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,288 @@
# Konzept: Ablösung von JavaFX durch Compose Multiplatform
> Status: Konzept, noch nicht umgesetzt. Zielrelease **1.50**. Alle Zeilenanker beziehen sich auf `nextMajorRelease/version1_50` und wurden gegen den Branch verifiziert (Stand 2026-09-25).
## Ziel
KST4Contest verlässt JavaFX und wird auf Compose Multiplatform umgestellt. Ergebnis ist eine wartbare, optisch aufgefrischte Desktop-Oberfläche bei **unverändertem Funktionsumfang**. Der fachliche Kern wird dabei von jeder UI-Technologie gelöst.
Drei Treiber, gleichrangig:
1. Die Oberfläche soll modern aussehen und sich modern anfühlen.
2. Der UI-Code soll wieder änderbar sein.
3. Die JavaFX-Abhängigkeit soll verschwinden.
## Nicht-Ziele
- **Kein UX-Neuentwurf.** 1.50 ist ein funktionsgleicher Port. Bedienabläufe, Fensteraufteilung und Funktionsumfang bleiben erhalten. Ein echter Neuentwurf der Bedienung ist Thema für 2.0.
- **Keine Mobile-App.** Die Modulgrenze wird so gezogen, dass ein späteres Mobile-Repo den Kern abgreifen könnte. Mehr nicht. Mobile ist in diesem Konzept Randbedingung, kein Lieferergebnis.
- **Keine Kotlin-Konvertierung des Kerns.** Siehe „Bestätigte Festlegungen".
- **Keine Protokolländerungen.** Kein Framing, kein Port, kein Transport, keine Normalisierung wird angefasst.
## Ausgangslage
Gemessen am Branchstand:
| Kennzahl | Wert |
|---|---|
| Java gesamt | 61.572 Zeilen, 196 Dateien |
| Kotlin | 0 Dateien |
| Dateien mit direktem JavaFX-Bezug | 33 |
| `Kst4ContestApplication.java` | 13.564 Zeilen, 387 Methoden |
| davon reines JavaFX-Gerüst | 82 `call()`-Cell-Factories, 64 `changed()`-Listener, 77 `handle()`-Eventhandler |
| `Platform.runLater` / `ObservableList` | 51 / 93 Vorkommen |
| Tests | 43 Dateien, 148 Testmethoden |
### Die JavaFX-Kopplung ist flacher als die Zahlen vermuten lassen
- `ChatMember`: **ein** gekoppeltes Feld, `StringProperty frequency` (`ChatMember.java:42`, Zugriff `:510`, `:516`, Neuanlage `:616`). Der Rest der Klasse ist reines Java.
- `ChatPreferences`: fünf Properties (`actualQTF` `:228`, `MYQRGFirstCat` `:266`, `MYQRGSecondCat` `:268`, `notify_optionalFrequencyPrefix` `:296`, `notify_DXCSrv_SpottersCallSign` `:298`) und vier `ObservableList<String>` für Listeneinstellungen (`:307`, `:308`, `:324`, `:325`).
- `ScoreService`: bereits sauber getrennt. Es existiert ein Snapshot-Modell, `topCandidatesFx` (`:45`, `:62`) ist explizit als FX-Projektion benannt, und der Klassenkommentar hält „No per-member Platform.runLater flooding" fest.
- `MessageBusManagementThread`: eine einzige Methodensignatur nimmt `ObservableList` entgegen (`:577`).
- `ChatController` ist der Brocken: sieben `ObservableList`-Felder (`:1164`, `:1742`, `:1759`, `:1768` mit der synchronisierten Hülle `:1775`, `:1776`, `:1777`, `:1817`) und eine `FilteredList`/`SortedList`-Pipeline (`:1745`, `:1749`, `:1751`, `:1756`, `:1772`, `:1773`).
Die Klasse dokumentiert ihr eigenes Muster bereits: der Kommentar ab `ChatController.java:1783` begründet, warum eine threadsichere Map die primäre Quelle ist und die `ObservableList` nur die gebündelte Projektion. Die hier vorgeschlagene Trennung setzt das fort, statt es zu erfinden.
### Die Karte hängt nur flach an Leaflet
`MapHtmlResources.java` nutzt aus der Leaflet-API lediglich `L.map`, `L.tileLayer`, `L.marker`, `L.divIcon`, `L.circleMarker`, `L.polyline`, `L.polygon`, `L.rectangle`, `L.layerGroup`, `L.latLng` und `L.latLngBounds`. Geometrie (`PathGeometryUtils`), Gitterplanung (`MaidenheadGridRenderPlanner`), Kachelbeschaffung (`TileProxyServer`), Terrain und Pfadanalyse liegen bereits vollständig in Java.
### Weitere betroffene Stellen
- `javafx.scene.media` wird ausschließlich in `Kst4ContestApplication.java` benutzt (`:33`, `:34`, `:6599`, `:6900`). MP3-Wiedergabe läuft ohnehin schon über `jlayer` in `PlayAudioUtils`.
- Lombok ist in `pom.xml` als Abhängigkeit deklariert, wird aber in keiner Quelldatei verwendet.
- `src/main/java/module-info.java` bindet das Projekt an JPMS; `pom.xml` enthält eine Enforcer-Regel, die die jpackage-Modulliste gegen Drift absichert.
## Bestätigte Festlegungen
| Punkt | Entscheidung |
|---|---|
| Zielrelease | 1.50. Die Compose-Migration findet auf `nextMajorRelease/version1_50` statt; 1.50 wird das Compose-Release |
| Funktionsumfang | Funktionsgleicher Port. UX-Neuentwurf frühestens 2.0 |
| Sprache Kern | `core` bleibt **Java 21**. Kotlin nur dort, wo Compose es erzwingt |
| Sprache UI | Kotlin, `app-desktop` |
| Build | Maven wird durch **Gradle** ersetzt. Compose Multiplatform wird offiziell nur mit Gradle unterstützt, Desktop-Packaging inbegriffen |
| Modulsystem | `module-info.java` entfällt. Compose Multiplatform liefert keine echten JPMS-Module |
| Packaging | Etappe 1 behält den direkten `jpackage`-Aufruf, nur aus Gradle statt aus Maven. Die Umstellung auf `compose.desktop.nativeDistributions` erfolgt in Etappe 3, wenn Kotlin und Compose ohnehin einziehen. DMG-Drag-Layout, Icon-Erzeugung, Flatpak und AUR werden dabei nachgezogen |
| Karte | Native Compose-Canvas-Umsetzung. Kein eingebettetes Chromium (KCEF/JCEF) |
| God-Klasse | `Kst4ContestApplication` wird in Phase 1 **nicht** zerschnitten. Compose löscht den Widget-Code ohnehin |
| Mobile | Nur als Modulgrenze berücksichtigt, nicht umgesetzt |
| Lombok | Entfällt (ungenutzt) |
| Entwickler | Marc (DO5AMF) und Philipp (DN9APW). Kein Termin |
## Zielarchitektur
```
kst4contest/
├── core/ Java 21, KEIN JavaFX, KEIN Compose
│ ├── model/ ChatMember, ChatMessage, ChatPreferences, Band …
│ ├── controller/ ChatController, MessageBus, Score, Reachability …
│ ├── net/ ON4KST, AirScout, Win-Test, UCXLog, PSTRotator, DXCluster
│ ├── map/ Geometrie, Terrain, Kachel-Proxy, Pfadanalyse
│ ├── audio/ Wiedergabe hinter einem Interface
│ └── observe/ Beobachtungs-Abstraktion
└── app-desktop/ Kotlin + Compose Multiplatform
├── ui/ Fenster, Panels, Tabellen, Dialoge
├── state/ UI-Zustand, Projektionen aus core
└── map/ Compose-Canvas-Karte
```
`core` hängt von keiner UI-Technologie ab. Das ist die Grenze, an der parallel gearbeitet werden kann, und dieselbe Grenze, die ein späteres Mobile-Repo abgreifen würde.
### Das Paket `core/observe`
Bewusst minimal. Kein Reaktiv-Framework, keine Bibliothek.
- **`ObservableValue<T>`** — `T get()`, `void addListener(Consumer<T>)`, `void removeListener(Consumer<T>)`.
- **`MutableValue<T> extends ObservableValue<T>`** — zusätzlich `void set(T)`.
- **`ObservableRoster<T>`** — **snapshotbasiert**: `List<T> snapshot()` liefert eine unveränderliche Liste; Listener erhalten die neue Liste. Es gibt **kein** inkrementelles Änderungsprotokoll. Das passt zum Neuzeichnungsmodell von Compose und ist ohne UI testbar.
- **`UiDispatcher`** — `void runOnUi(Runnable)` und `boolean isUiThread()`.
`UiDispatcher` ist der Schlüssel der gesamten Umstellung. In Phase 1 wird er von `Platform::runLater` und `Platform::isFxApplicationThread` implementiert, in Phase 2 vom Compose-Main-Dispatcher. Die Invariante aus `AGENTS.md` bleibt dabei wortgleich gültig: Worker-Threads mutieren keine UI-gebundenen Sammlungen, UI-sichtbare Änderungen laufen über den Dispatcher.
### Was aus der JavaFX-Pipeline wird
`FilteredList` und `SortedList` werden zu einer **reinen Funktion** in `core`: aus Mitgliederliste, Filterprädikaten und Komparator entsteht eine unveränderliche Ergebnisliste. Die Stationslisten-Logik wird damit erstmals ohne laufende UI testbar. Das ist der wesentliche fachliche Gewinn von Phase 1.
Die vorhandene Bündelung und Entprellung (`flushPendingChatMessagesToUi`, das Coalescing der Mitgliederliste) **bleibt erhalten**. Sie ist Leistungsschutz bei mehreren tausend ON4KST-Nutzern, kein JavaFX-Beiwerk.
## Etappen
Jede Etappe ist für sich abgeschlossen, lieferbar und bekommt einen eigenen Umsetzungsplan.
### Etappe 1 — Gradle, zwei Module, JPMS entfernen, Packaging
**Ziel:** Die unveränderte JavaFX-Anwendung baut, testet und paketiert unter Gradle.
**Umfang**
- Gradle-Wrapper ersetzt `mvnw`/`mvnw.cmd`.
- Aufteilung in die Module `core` und `app-desktop`; in dieser Etappe enthält `app-desktop` noch die JavaFX-Oberfläche.
- **`core` darf in dieser Etappe noch von JavaFX abhängen.** `ChatController`, `ChatMember`, `ChatPreferences` und `ScoreService` benutzen es; Etappe 2 entfernt die Abhängigkeit. Etappe 1 bleibt damit eine reine Build-Umstellung ohne fachliche Änderung.
- Paketnamen und `import`-Anweisungen ändern sich nicht. Gradle-Module erzwingen keine Paketumbenennung; die Aufteilung ist ein Verschieben von Verzeichnissen.
- `src/main/java/module-info.java` wird entfernt; die Enforcer-Regel zur jpackage-Modulliste in `pom.xml` entfällt mit ihr.
- SpotBugs- und PMD-Regelsätze wandern mit; `pmd-ruleset.xml` bleibt unverändert gültig.
- Lombok wird aus den Abhängigkeiten entfernt.
- Packaging für Windows, Linux und macOS ruft `jpackage` weiterhin direkt auf, künftig aus einer Gradle-Aufgabe. Das DMG-Drag-Layout, die Icon-Erzeugung, Flatpak und AUR bleiben inhaltlich unverändert; nur die Herkunft der Eingabedateien wechselt von `target/dist-libs` auf `app-desktop/build/dist-libs`.
- Die Projektversion wandert nach `gradle.properties`. Zehn Stellen in fünf Workflows und zwei PKGBUILDs lesen sie heute aus `pom.xml` und müssen umgestellt werden, bevor `pom.xml` entfällt.
- Die GitHub-Workflows unter `.github/` werden auf Gradle umgestellt.
**Abnahmekriterium:** Alle 148 vorhandenen Tests laufen unter Gradle. Auf allen drei Plattformen entsteht ein installierbares Paket, das sich wie das bisherige verhält. Stable-, Beta- und Nightly-Strecke funktionieren unverändert.
### Etappe 2 — `core` von JavaFX befreien
**Ziel:** `core` compiliert ohne JavaFX auf dem Klassenpfad. Die JavaFX-Oberfläche läuft unverändert und beweist, dass die Schnittstelle trägt.
**Umfang**
- `core/observe` entsteht wie oben beschrieben.
- `ChatMember.frequency` wird von `StringProperty` auf `MutableValue<String>` umgestellt (`ChatMember.java:42`, `:510`, `:516`, `:616`).
- Die fünf Properties und vier `ObservableList<String>` in `ChatPreferences` werden auf `MutableValue` beziehungsweise `ObservableRoster` umgestellt.
- Die sieben `ObservableList`-Felder in `ChatController` werden zu `ObservableRoster`; die `FilteredList`/`SortedList`-Pipeline wird durch die reine Filter-/Sortierfunktion ersetzt.
- `ScoreService` gibt seinen Snapshot über `ObservableRoster` heraus statt über `topCandidatesFx`; `Platform.runLater` wird durch `UiDispatcher` ersetzt.
- `MessageBusManagementThread.checkListForChatMemberIndexByCallSign` (`:577`) nimmt eine `List<ChatMember>` statt `ObservableList`.
- Audio wandert nach `core/audio` hinter ein Interface. `javafx.scene.media` (`Kst4ContestApplication.java:33`, `:34`, `:6599`, `:6900`) entfällt; MP3 über `jlayer`, WAV über `javax.sound.sampled`.
- `app-desktop` stellt die JavaFX-Implementierung von `UiDispatcher` bereit und adaptiert `ObservableRoster` auf `ObservableList` für die bestehenden `TableView`.
**Absicherung:** Jede herausgelöste Einheit bekommt **vorher** einen Test, der das heutige Verhalten festschreibt — insbesondere Filterung und Sortierung der Stationsliste, die heute in `FilteredList`/`SortedList` steckt und nicht testbar ist.
**Abnahmekriterium:** `core` hat keine JavaFX-Abhängigkeit mehr. Die Anwendung verhält sich unverändert: Stationsliste, Chat-Tabs, Prioritäten, Skeds, Cluster, Worked-Zustand, Profilwechsel.
### Etappe 3 — `DataTable` und Einstellungsfenster in Compose
**Ziel:** Das erste Compose-Fenster ist im Produkt.
**Umfang**
Der wesentliche Baustein dieser Etappe ist eine wiederverwendbare **`DataTable`**-Komponente. Compose kennt kein `TableView`, und die Anwendung hat fünf Tabellen: `TableView<ChatMember>`, `TableView<ChatMessage>`, `TableView<ClusterMessage>`, `TableView<String>` und die Top-Prioritätenliste. Alle brauchen dasselbe:
- Spaltenbreiten mit stabilen Tabellen- und Spalten-IDs, wie sie `preferences.xml` Version 7 bereits kennt;
- Sortierung nach Spalte;
- Zeilenauswahl und Kontextmenü;
- Zeilen- und Zell-Styling, heute `PrivateMessageRowStyleResolver`, `MessageTextTableCell`, `TruncatedTextTableCell`, `TruncatedTextTooltipSupport`.
Umgesetzt als `LazyColumn` mit eigener Kopfzeile. Wird diese Komponente gut, ist der Rest der Portierung Fleißarbeit; wird sie schlecht, zieht sich das durch die gesamte Anwendung.
Mit dieser Etappe ziehen Kotlin und das Compose-Gradle-Plugin ein. Das Packaging wird dabei von den direkten `jpackage`-Aufrufen auf `compose.desktop.nativeDistributions` umgestellt — der Zeitpunkt ist hier richtig, weil JavaFX dann nicht mehr über `jlink` gebündelt werden muss.
Danach das Einstellungsfenster mit seinen zwölf Reitern: Station, Log synch, TRX synch, Airscout, Notification, Shortcuts, Macros, Beacon, Messagehandling, Workedstn database, GUI, Profiles. Es ist in sich abgeschlossen, hat wenig Domänenkopplung und eignet sich deshalb als Lernstück.
**Abnahmekriterium:** Das Einstellungsfenster ist vollständig in Compose, alle zwölf Reiter sind funktionsgleich, und geänderte Einstellungen landen unverändert in `preferences.xml`.
> Nachzuschärfen nach Etappe 2: der genaue Zuschnitt der `DataTable`-Schnittstelle. Er hängt davon ab, wie sich `ObservableRoster` in der Praxis anfühlt.
### Etappe 4 — Profil-Dialog, Update-Fenster, Monitor-Fenster
**Ziel:** Drei weitere Fenster portiert; die Muster aus Etappe 3 sind bestätigt.
**Umfang**
- `OperatorProfilePickerDialog` und `OperatorProfileSettingsPane`.
- Das Update-Fenster (`Kst4ContestApplication.java:9242`).
- Das Monitor-Fenster für Cluster und QSOs (`:9180`), das die `DataTable` erstmals mit Live-Daten belastet.
Die Layout-IDs für das Monitor-Fenster bleiben von denen des Hauptfensters getrennt, wie in `PROJECT_CONTEXT.md` festgelegt.
**Abnahmekriterium:** Die drei Fenster sind funktionsgleich. Fenstergrößen, Positionen und Spaltenbreiten werden unverändert entprellt gespeichert und beim Herunterfahren geschrieben.
### Etappe 5 — Hauptfenster
**Ziel:** JavaFX wird nur noch für die Karte benötigt.
**Umfang**
- Stationsliste mit Filterleiste und Prioritätenspalten.
- Die drei Chat-Reiter „Public messages", „DXCluster messages", „QSO of the other".
- Der Info-Bereich zur ausgewählten Station (`generateFurtherInfoAbtSelectedCallsignBP`, `Kst4ContestApplication.java:707`) samt seinem vertikalen `SplitPane` und dessen gespeicherten Divider-Positionen.
- Die Top-Prioritätenliste (`initTopPriorityListPane`, `:4416`).
- Die Statusleiste mit ihren Indikatoren: Sked-Warnung (`:5868`), Verbindungszustand (`:5887`), Band-Upgrade (`:6045`).
- `TimelineView`.
- Menüleiste, einschließlich der macOS-Systemmenüleiste.
**Profilwechsel:** Der heutige Mechanismus reißt die Laufzeit ab und baut eine **neue** `Kst4ContestApplication`-Instanz auf, weil viele Bedienelemente inline initialisierte Instanzfelder sind. Dieser Grund entfällt in Compose — der Zustand hängt nicht mehr an Feldern einer Riesenklasse. Der Wechsel wird zum Austausch des Zustandsobjekts. `shutdownRuntime()` bleibt trotzdem verantwortlich für alles, was eine Verbindung überdauert: ON4KST-Supervisor, Sked-Erinnerung, Reachability-Executor, PSTRotator-Wiederholung, Kachel-Proxy, Zeitgeber. `ApplicationConstants.sessionRuntimeUniqueId` wird weiterhin nicht neu erzeugt.
**Abnahmekriterium:** Das Hauptfenster ist funktionsgleich. Der Profilwechsel im laufenden Betrieb funktioniert, ohne Listener doppelt zu registrieren und ohne Ressourcen zu halten.
> Nachzuschärfen nach Etappe 4: ob `Platform.setImplicitExit(false)` und `ApplicationRuntimeLauncher.exitApplication()` in der Compose-Fassung noch eine Entsprechung brauchen.
### Etappe 6 — Karte und Entfernung von JavaFX
**Ziel:** 1.50. Keine JavaFX-Abhängigkeit mehr im Projekt.
**Umfang**
Die Karte wird als Compose-Canvas neu umgesetzt:
- Web-Mercator-Projektion, Zoomstufen und Schwenken wie bisher;
- Kacheln über den bestehenden `TileProxyServer`;
- Marker, Linien, Polygone, Rechtecke und Kreismarker direkt gezeichnet, entsprechend der heutigen flachen Leaflet-Nutzung;
- Maidenhead-Gitter über den bestehenden `MaidenheadGridRenderPlanner`;
- Pfaddarstellung und Geländeprofil (`PathProfileChart`) als Compose-Zeichnung;
- bildschirmbasiertes Clustering als Vorschalt-Schritt vor dem Zeichnen. `GUIstationMapClusteringEnabled` bleibt eine Layout-Einstellung unterhalb `guiOptions` mit Vorgabewert `true`.
`MapHtmlResources`, `StationMapBridge` und die JavaScript-Brücke entfallen ersatzlos. `StationMapView` wird zur Compose-Komponente.
Zum Abschluss werden die JavaFX-Abhängigkeiten aus dem Build entfernt und `opens kst4contest.view.map to javafx.web` verliert seinen Gegenstand.
**Abnahmekriterium:** Die Karte zeigt dieselben Informationen wie die Leaflet-Fassung. Offline-DEM, Terrain-Pakete und der Profil-Cache funktionieren unverändert. Im Projekt existiert keine JavaFX-Abhängigkeit mehr.
> Nachzuschärfen nach Etappe 5: Leistungsverhalten des Canvas bei mehreren tausend Markern. Falls das Zeichnen nicht trägt, ist eine Vorabaggregation der Marker im Kern der Ausweg.
## Invarianten — was unverändert bleiben muss
Diese Punkte sind in `AGENTS.md` und `PROJECT_CONTEXT.md` festgelegt und werden von der Umstellung nicht berührt.
**Chat-Identität**
- Vollständiges sichtbares Rufzeichen plus Kategorie bildet die Identität eines Chat-Teilnehmers.
- Basisruf-Normalisierung nur für explizit basisrufweite Funktionen.
- Worked-Zustand wird über Suffixvarianten desselben Basisrufs geteilt.
- Suffixe werden nicht global als Band, Kategorie oder Frequenz gedeutet.
**Bänder und Verfügbarkeit**
- Kategorien 2 und 3 sind die Hauptkategorien; unerwartete Werte müssen sicher fehlschlagen.
- `NOT-QRV` hat Vorrang vor positiven Band-Hinweisen.
- Fehlende Frequenz-, QRB- oder QTF-Werte bleiben unbekannt. `null` bedeutet nicht verfügbar, nicht null.
- Kein stilles Zurückfallen auf feste 144 MHz.
**Externe Schnittstellen**
- CR/LF-Framing, XML-Framing, Ports und Transporte bleiben unverändert.
- Die lokale DX-Cluster-Ausgabe behält ihre feste 75-Zeichen-Zeile, das DX-Rufzeichen ab Spalte 27, den 30-Zeichen-Kommentar ab Spalte 40 und die UTC-Zeit ab Spalte 71, gefolgt von zwei BEL und CRLF.
- Unerwartete Eingaben beenden keinen Verarbeitungs- oder UI-Thread.
**Persistenz**
- `preferences.xml` bleibt in Version 7, einschließlich der verwalteten Spaltenbreiten unterhalb `guiOptions` und der Regel, dass Elternspalten ihre Breite aus den Blattspalten ableiten.
- Selektives Schreiben aktualisiert die Datei auf der Platte, bewahrt unbekannte XML-Knoten und schreibt keine unbestätigten funktionalen Einstellungen. **Save Settings** bleibt der vollständige Schreiber.
- Vollständige und selektive Schreibvorgänge sind synchronisiert und ersetzen `preferences.xml` atomar.
- Operator-Profile behalten ihr Verhalten: Wurzelprofil flach, weitere Profile unter `profiles/<profileId>/`, `profiles.xml` wird verzögert angelegt, die Registrierung speichert niemals einen Pfad, ein neues Profil bekommt eine **leere** Datenbank.
- Das SQLite-Schema und die Worked-Semantik samt Dreitagesablauf bleiben unverändert.
- Simplelogfile-Verhalten bleibt unverändert, einschließlich des minütlichen Lesens und der Tatsache, dass daraus abgeleiteter Worked-Zustand nicht in SQLite landet.
**Quelltext-Konventionen**
- Kommentare und Javadoc ausschließlich auf Englisch.
- Log-, Protokoll- und API-Literale in ihrer kanonischen Form.
- Kommunikation mit Marc auf Deutsch; Commit-Nachrichten knapp auf Englisch.
## Risiken
| Risiko | Bewertung | Umgang |
|---|---|---|
| `DataTable` wird der Engpass | hoch | Etappe 3 baut sie zuerst und belastet sie in Etappe 4 mit Live-Daten, bevor das Hauptfenster davon abhängt |
| Packaging auf drei Plattformen neu | hoch | Etappe 1 macht das zuerst, mit unveränderter JavaFX-App als Vergleichsmaßstab |
| Canvas-Leistung bei vielen Markern | mittel | Ausweg ist Vorabaggregation im Kern; wird in Etappe 6 gemessen, nicht angenommen |
| Compose-Erfahrung fehlt bei beiden Entwicklern | mittel | Das Einstellungsfenster als erstes Lernstück ist bewusst ungefährlich gewählt |
| Funktionsgleichheit ist schwer nachweisbar | mittel | Vergleich Fenster für Fenster gegen die noch laufende JavaFX-Fassung; Protokoll und Persistenz sind durch `core`-Tests gedeckt |
| 1.50 verschiebt sich erheblich | angenommen | Bewusste Entscheidung. Kein Termin gesetzt |
## Offene Punkte
Diese Fragen werden bewusst erst beantwortet, wenn die vorangehende Etappe Erfahrung geliefert hat. Sie blockieren Etappe 1 und 2 nicht.
1. Genauer Zuschnitt der `DataTable`-Schnittstelle — nach Etappe 2.
2. Ob `Platform.setImplicitExit(false)` in Compose eine Entsprechung braucht — nach Etappe 4.
3. Aggregationsstrategie für Kartenmarker, falls das Canvas-Zeichnen nicht trägt — nach Etappe 5.
4. Ob das Geländeprofil als eigene Compose-Zeichnung oder über eine Diagrammbibliothek entsteht — nach Etappe 5.