mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 05:42:58 +02:00
Remove the implementation plans now that every stage is executed
Thirteen plans, all of them step-by-step instructions for work that is committed. A plan left behind reads to the next person like an open intention, and these describe a build that no longer exists -- the first still explains how to introduce Gradle. The specifications stay. They say why the architecture is what it is, which the code cannot, and three of them are still argued from. The live-findings note stays too: CLAUDE.md sends people to it, and its hardest lesson -- that a blocking wait on the AWT event thread deadlocks the interface while the build stays green -- is written down nowhere else. Its sweep script stays as well. The note argues explicitly that the script belongs beside the finding so the next reader can measure rather than believe, and removing it would contradict the reasoning it illustrates. Two references in the note and one in the JavaFX-removal spec pointed at plans and now say where the reasoning went instead.
This commit is contained in:
17 files changed
+10
-16159
No files matched your search
@@ -29,7 +29,7 @@ erzeugt und **nicht ausgeliefert** wird). Paketnamen sind über `core` und
|
||||
|
||||
## Sprache
|
||||
|
||||
Kommunikation mit Marc auf Deutsch, in **neuer Rechtschreibung** — `muss` und `dass`,
|
||||
Kommunikation auf Deutsch, in **neuer Rechtschreibung** — `muss` und `dass`,
|
||||
nicht `muß` und `daß`. Quelltextkommentare und Javadoc bleiben ausschließlich Englisch.
|
||||
|
||||
Oberflächentexte stehen seit Etappe 9 in `app-desktop/src/main/i18n/strings_<code>.properties`
|
||||
@@ -38,8 +38,11 @@ Regel, was übersetzt wird, steht in `docs/PROJECT_CONTEXT.md` unter „Importan
|
||||
|
||||
## Laufende Arbeit
|
||||
|
||||
Spezifikationen liegen unter `docs/superpowers/specs/`, Umsetzungspläne unter
|
||||
`docs/superpowers/plans/`. Die Ablösung von JavaFX durch Compose Multiplatform ist
|
||||
Spezifikationen liegen unter `docs/superpowers/specs/` und bleiben: sie sagen, **warum** die
|
||||
Architektur so ist, und das steht nirgends sonst. Umsetzungspläne werden nach der Ausführung
|
||||
entfernt — sie sind Arbeitsanweisungen für erledigte Arbeit, und ein liegengebliebener Plan
|
||||
liest sich für den Nächsten wie eine offene Absicht. Was aus ihnen zu behalten war, steht in
|
||||
den Commit-Nachrichten und in `docs/PROJECT_CONTEXT.md`. Die Ablösung von JavaFX durch Compose Multiplatform ist
|
||||
**abgeschlossen**: unter `src/main` steht kein `javafx.` mehr, und `org.openjfx` ist
|
||||
aus `app-desktop/build.gradle.kts` und `gradle/libs.versions.toml` entfernt. Die
|
||||
Begründungen und die harten Lehren stehen in
|
||||
|
||||
@@ -43,7 +43,8 @@ Erhoben am 1. Oktober 2026 auf Linux, Branch `nextMajorRelease/version1_50`.
|
||||
> und darum nicht testbar war). Erstmals mit Tests: vier, darunter „ein Stoß von 20
|
||||
> Ereignissen wird zu einem Schreibvorgang". **Offen (Etappe 6):** die 11 Restdateien, die 19
|
||||
> Steuerelemente, `org.openjfx` aus Build und Versionskatalog.
|
||||
> Plan: `docs/superpowers/plans/2026-10-03-javafx-entfernen-teil4-dispatcher-lebenszyklus.md`.
|
||||
> Behoben in Etappe 4/5; der Plan ist nach seiner Ausführung entfernt worden, die
|
||||
> Begründung steht in der Commit-Nachricht und hier.
|
||||
|
||||
> **Etappe 6 abgeschlossen am 2026-10-04 — JavaFX ist raus.** Nachgewiesen, nicht
|
||||
> geschlussfolgert: `grep -rl "javafx\." core/src/main app-desktop/src/main` ist leer, es
|
||||
@@ -153,7 +154,8 @@ Erhoben am 1. Oktober 2026 auf Linux, Branch `nextMajorRelease/version1_50`.
|
||||
> `OperatorProfilePickerWindow` (eigener Faden plus `CountDownLatch`) würde dort verklemmen,
|
||||
> weil es genau den Faden blockiert, auf dem das neue Fenster zeichnen will. Der
|
||||
> Einzelkonstruktor `ComposeDialog(Dialog.ModalityType)` existiert und wird benutzt.
|
||||
> Plan: `docs/superpowers/plans/2026-10-03-javafx-entfernen-teil4-dispatcher-lebenszyklus.md`.
|
||||
> Behoben in Etappe 4/5; der Plan ist nach seiner Ausführung entfernt worden, die
|
||||
> Begründung steht in der Commit-Nachricht und hier.
|
||||
|
||||
> **Teil 3 abgeschlossen am 2026-10-02.** Der gesamte durchsetzte JavaFX-Aufbau in
|
||||
> `Kst4ContestApplication` ist gelöscht; die Klasse ist von 11.179 auf ~5.100 Zeilen
|
||||
|
||||
@@ -1,991 +0,0 @@
|
||||
# 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
@@ -1,893 +0,0 @@
|
||||
# Etappe 3a — Kotlin, Compose und Packaging: 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:** Kotlin und Compose Multiplatform sind im Projekt, das Packaging läuft über `compose.desktop.nativeDistributions`, und **ein echtes Fenster der Anwendung ist in Compose geschrieben**.
|
||||
|
||||
**Architektur:** `app-desktop` wird ein gemischtes Java/Kotlin-Modul. Die JavaFX-Oberfläche bleibt vollständig erhalten; ein einzelnes, kleines Fenster — der Operator-Profil-Auswahldialog, 145 Zeilen — wird durch eine Compose-Fassung ersetzt. Damit ist bewiesen, dass beide Toolkits im selben Prozess koexistieren, was die Voraussetzung für die Etappen 3b, 4 und 5 ist.
|
||||
|
||||
**Tech-Stack:** Kotlin 2.2.0, Compose Multiplatform 1.8.2, Gradle 9.7.1, Java 21. Keine Änderung an `core`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-25-compose-migration-design.md`
|
||||
|
||||
## Warum Etappe 3 in zwei Pläne geteilt ist
|
||||
|
||||
Das Spec beschreibt Etappe 3 als eine Etappe: Kotlin und Compose einziehen, Packaging umstellen, die `DataTable` bauen, das Einstellungsfenster portieren. Gemessen am Code ist das mehr als Etappe 1 und 2 zusammen:
|
||||
|
||||
| Teil | Umfang |
|
||||
|---|---|
|
||||
| Einstellungsfenster | 2341 Zeilen in `Kst4ContestApplication` plus 488 in `OperatorProfileSettingsPane` |
|
||||
| `DataTable`-Vorarbeit | `TableLayoutManager` 280, `MessageTextTableCell` 326, `TruncatedTextTableCell` 109, `PrivateMessageRowStyleResolver` 83, `TruncatedTextTooltipSupport` 28 |
|
||||
| Toolchain | Sprache, Toolkit und Packaging auf drei Plattformen |
|
||||
|
||||
**Dieser Plan (3a)** liefert die Toolchain und ein kleines, echtes Compose-Fenster. **Plan 3b** liefert die `DataTable` und das Einstellungsfenster. Die Teilung folgt einer Abhängigkeit: die `DataTable` lässt sich nur an einem echten Verbraucher prüfen, und der kleinste echte Verbraucher ist ein Reiter des Einstellungsfensters. Beide gehören deshalb zusammen — die Toolchain nicht.
|
||||
|
||||
## Das Risiko, das dieser Plan zuerst prüft
|
||||
|
||||
Compose Desktop zeichnet über Skiko in ein AWT-Fenster und betreibt seinen eigenen Ereignis-Thread. JavaFX hat seinen eigenen. Ob ein Compose-Fenster aus einer laufenden JavaFX-Anwendung aufgehen kann, ohne dass eines von beiden stehen bleibt, ist **nicht belegt** — und die Etappen 3b, 4 und 5 setzen es voraus, weil sie Fenster einzeln umstellen.
|
||||
|
||||
Aufgabe 3 klärt das, bevor Arbeit darauf aufbaut. **Scheitert sie, endet dieser Plan dort**, und die Etappenfolge im Spec muss neu gedacht werden: dann bleibt nur ein Umschalttag, an dem die gesamte Oberfläche wechselt. Das wäre eine Änderung am Spec, nicht am Plan.
|
||||
|
||||
Zusätzlich gilt für den Profilwechsel: `Platform.setImplicitExit(false)` hält den Prozess am Leben, wenn alle JavaFX-Fenster zu sind. Ein offenes Compose-Fenster verlängert die Prozesslebensdauer auf eine zweite, unabhängige Weise. Aufgabe 6 prüft, dass das Beenden weiterhin über `ApplicationRuntimeLauncher.exitApplication()` geht und der Prozess wirklich endet.
|
||||
|
||||
## Globale Randbedingungen
|
||||
|
||||
- Java 21, Gradle 9.7.1. `core` wird **nicht** angefasst und bleibt frei von UI-Technologie.
|
||||
- Die JavaFX-Oberfläche bleibt funktionsfähig. Diese Etappe ersetzt genau ein Fenster.
|
||||
- Versionen zu Beginn von Aufgabe 1 gegen die aktuellen Veröffentlichungen prüfen; die hier genannten sind der Stand vom 2026-09-27.
|
||||
- Kommentare und Javadoc auf Englisch, auch in Kotlin (KDoc). Kommunikation auf Deutsch.
|
||||
- Die Invarianten aus dem Spec-Abschnitt „Invarianten" gelten unverändert, insbesondere `preferences.xml` Version 7 und das Verhalten der Operator-Profile.
|
||||
- **`Platform.runLater` und `uiDispatcher.runOnUi` sind nicht dasselbe.** Der Dispatcher führt inline aus, wenn der Aufrufer schon auf dem UI-Thread ist. Wer innerhalb des UI-Threads nach hinten verschieben will, braucht `Platform.runLater` ausdrücklich. Siehe `prepareCqTextForCallsign`; das hat in Etappe 2 einen Fehler gekostet.
|
||||
|
||||
## Prüfschwerpunkte
|
||||
|
||||
Fünf Fehlerklassen, die kein Übersetzungslauf zeigt.
|
||||
|
||||
1. **Die beiden Toolkits blockieren sich.** Ein Compose-Fenster, das den JavaFX-Thread anhält oder umgekehrt, äußert sich als hängende Oberfläche, nicht als Ausnahme. → Aufgabe 3.
|
||||
2. **Der Prozess endet nicht mehr.** Mit `Platform.setImplicitExit(false)` und einem zusätzlichen AWT-Fenster kann ein Nicht-Daemon-Thread den Prozess überleben lassen. Der Operator schließt die Anwendung und sie läuft weiter. → Aufgabe 6.
|
||||
3. **Das Paket wird größer oder unvollständig.** `compose.desktop.nativeDistributions` ermittelt die JDK-Module selbst. Die in Etappe 1 mühsam ermittelte Liste — `jdk.jsobject` für die Karten-Brücke, `jdk.unsupported` für den Marlin-Renderer — kann dabei verloren gehen, und das zeigt sich erst beim Start des Pakets. → Aufgabe 5.
|
||||
4. **Die Schriftgröße und das Erscheinungsbild springen.** Compose kennt die CSS-Themen des Projekts nicht. Ein Compose-Fenster neben JavaFX-Fenstern kann anders skaliert aussehen, besonders bei der einheitlichen Basis-Schriftgröße aus 1.50. → Aufgabe 6.
|
||||
5. **Der Profilwechsel bricht.** Der Auswahldialog läuft im Startpfad, **vor** dem Aufbau der Hauptoberfläche (`OperatorProfileBootstrap`). Ein Compose-Fenster dort verschiebt die Reihenfolge von Toolkit-Initialisierungen. → Aufgabe 6.
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 1: Kotlin ins Projekt
|
||||
|
||||
**Dateien:**
|
||||
- Ändern: `gradle/libs.versions.toml`
|
||||
- Ändern: `app-desktop/build.gradle.kts`
|
||||
- Anlegen: `app-desktop/src/main/kotlin/kst4contest/view/BuildInfo.kt`
|
||||
- Test: `app-desktop/src/test/kotlin/kst4contest/view/BuildInfoTest.kt`
|
||||
|
||||
**Schnittstellen:**
|
||||
- Erzeugt: das Quellverzeichnis `app-desktop/src/main/kotlin`, von allen folgenden Aufgaben benutzt.
|
||||
|
||||
- [ ] **Schritt 1: Aktuelle Versionen prüfen**
|
||||
|
||||
Nachsehen, welche Fassungen aktuell sind:
|
||||
- Kotlin (Stand 2026-09-27: **2.2.0** neueste stabile)
|
||||
- Compose Multiplatform (Stand 2026-09-27: **1.8.2** neueste stabile)
|
||||
|
||||
**Kotlin darf hier nicht beliebig alt sein:** das Kotlin-Gradle-Plugin unterstützt Gradle 9 erst ab 2.2. Das Projekt läuft auf Gradle 9.7.1, also ist 2.2.0 die Untergrenze, nicht nur die neueste Wahl.
|
||||
|
||||
Die beiden müssen zueinander passen: jede Compose-Multiplatform-Fassung nennt in ihren Veröffentlichungshinweisen die Kotlin-Fassungen, mit denen sie arbeitet. Weicht etwas ab, die passende Kombination verwenden und die Abweichung im Ledger vermerken.
|
||||
|
||||
- [ ] **Schritt 2: Versionskatalog erweitern**
|
||||
|
||||
In `gradle/libs.versions.toml` unter `[versions]` ergänzen:
|
||||
|
||||
```toml
|
||||
kotlin = "2.2.0"
|
||||
composeMultiplatform = "1.8.2"
|
||||
```
|
||||
|
||||
und unter `[plugins]`:
|
||||
|
||||
```toml
|
||||
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
|
||||
```
|
||||
|
||||
> Das Compose-Compiler-Plugin gehört seit Kotlin 2.0 zur Kotlin-Auslieferung und trägt deshalb die Kotlin-Version, nicht die von Compose. Das sind zwei verschiedene Versionsnummern für zwei verschiedene Dinge.
|
||||
|
||||
- [ ] **Schritt 3: Kotlin in `app-desktop` einschalten**
|
||||
|
||||
In `app-desktop/build.gradle.kts` den `plugins`-Block erweitern:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
java
|
||||
application
|
||||
alias(libs.plugins.javafx)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
}
|
||||
```
|
||||
|
||||
und nach dem `javafx`-Block ergänzen:
|
||||
|
||||
```kotlin
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: Eine Kotlin-Datei mit echtem Inhalt schreiben**
|
||||
|
||||
Kein leeres Gerüst: diese Datei soll etwas tun, das die Oberfläche später braucht.
|
||||
|
||||
`app-desktop/src/main/kotlin/kst4contest/view/BuildInfo.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view
|
||||
|
||||
/**
|
||||
* Build identity shown in window titles and in the about text.
|
||||
*
|
||||
* Reads the version the Gradle build stamped into the jar manifest and falls
|
||||
* back to a marker that is obviously not a release, so an unstamped build can
|
||||
* never be mistaken for one.
|
||||
*/
|
||||
object BuildInfo {
|
||||
|
||||
const val UNKNOWN_VERSION: String = "dev-unstamped"
|
||||
|
||||
val version: String
|
||||
get() = BuildInfo::class.java.`package`?.implementationVersion ?: UNKNOWN_VERSION
|
||||
|
||||
fun windowTitle(base: String): String = "$base $version"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 5: Den fehlschlagenden Test schreiben**
|
||||
|
||||
`app-desktop/src/test/kotlin/kst4contest/view/BuildInfoTest.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
|
||||
class BuildInfoTest {
|
||||
|
||||
@Test
|
||||
fun `an unstamped build reports a version that cannot be mistaken for a release`() {
|
||||
// Tests run from class directories, where no jar manifest exists.
|
||||
assertEquals(BuildInfo.UNKNOWN_VERSION, BuildInfo.version)
|
||||
assertFalse(BuildInfo.version.first().isDigit(),
|
||||
"an unstamped version must not look like a release number")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the window title carries the version`() {
|
||||
val title = BuildInfo.windowTitle("KST4Contest")
|
||||
assertTrue(title.startsWith("KST4Contest "))
|
||||
assertTrue(title.endsWith(BuildInfo.version))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 6: Test laufen lassen und Fehlschlag sehen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.BuildInfoTest'`
|
||||
Erwartet: FAIL — Kotlin-Testquellen werden noch nicht übersetzt, oder `BuildInfo` fehlt.
|
||||
|
||||
- [ ] **Schritt 7: Kotlin-Testquellen einschalten**
|
||||
|
||||
Das Kotlin-Plugin richtet `src/test/kotlin` selbst ein. Schlägt der Lauf weiterhin fehl, prüfen:
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:dependencies --configuration testCompileClasspath | grep -i kotlin`
|
||||
Erwartet: `kotlin-stdlib` ist enthalten.
|
||||
|
||||
- [ ] **Schritt 8: Test laufen lassen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.BuildInfoTest'`
|
||||
Erwartet: PASS, 2 Testfälle.
|
||||
|
||||
- [ ] **Schritt 9: Gesamtlauf**
|
||||
|
||||
Ausführen: `./gradlew clean build`
|
||||
Erwartet: BUILD SUCCESSFUL. Vorher sind es **255** Testfälle, jetzt **257**, 0 Fehler.
|
||||
|
||||
Ausführen: `grep -rn "javafx" core/src | wc -l`
|
||||
Erwartet: `0` — Kotlin darf `core` nicht berühren.
|
||||
|
||||
- [ ] **Schritt 10: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Compile Kotlin in the desktop module"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 2: Compose-Plugin, nur Übersetzung
|
||||
|
||||
Noch kein Fenster, noch kein Packaging. Nur: Compose-Code übersetzt.
|
||||
|
||||
**Dateien:**
|
||||
- Ändern: `app-desktop/build.gradle.kts`
|
||||
- Anlegen: `app-desktop/src/main/kotlin/kst4contest/view/compose/Theme.kt`
|
||||
|
||||
**Schnittstellen:**
|
||||
- Erzeugt: `kst4contest.view.compose.Kst4ContestTheme`, von Aufgabe 4 und von Plan 3b benutzt.
|
||||
|
||||
- [ ] **Schritt 1: Plugins ergänzen**
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
java
|
||||
application
|
||||
alias(libs.plugins.javafx)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
}
|
||||
```
|
||||
|
||||
und im `dependencies`-Block:
|
||||
|
||||
```kotlin
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.material3)
|
||||
```
|
||||
|
||||
- [ ] **Schritt 1b: Googles Maven-Repository ergänzen**
|
||||
|
||||
Compose Multiplatform holt `androidx.lifecycle` und `androidx.annotation` aus Googles Repository; auf Maven Central liegen sie nicht. Ohne diesen Eintrag scheitert schon das Auflösen des Klassenpfads mit `Could not find androidx.lifecycle:lifecycle-common`.
|
||||
|
||||
In `build.gradle.kts` im `repositories`-Block von `allprojects` ergänzen:
|
||||
|
||||
```kotlin
|
||||
google()
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: Ein Thema schreiben, das die Vorgaben des Projekts aufnimmt**
|
||||
|
||||
`app-desktop/src/main/kotlin/kst4contest/view/compose/Theme.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/**
|
||||
* The application theme for Compose windows.
|
||||
*
|
||||
* Release 1.50 gave the JavaFX side one base font size per operator profile.
|
||||
* Compose windows take the same value so a Compose window next to a JavaFX one
|
||||
* does not look scaled differently.
|
||||
*
|
||||
* @param darkMode the operator's configured day/evening design
|
||||
* @param baseFontSizeSp the configured base font size, in scale-independent points
|
||||
*/
|
||||
@Composable
|
||||
fun Kst4ContestTheme(
|
||||
darkMode: Boolean,
|
||||
baseFontSizeSp: Float,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colors = if (darkMode) darkColorScheme() else lightColorScheme()
|
||||
val typography = MaterialTheme.typography.scaledTo(baseFontSizeSp)
|
||||
|
||||
MaterialTheme(colorScheme = colors, typography = typography, content = content)
|
||||
}
|
||||
```
|
||||
|
||||
Die Hilfsfunktion `scaledTo` wird in Schritt 3 geschrieben.
|
||||
|
||||
- [ ] **Schritt 3: Die Schriftskalierung schreiben, mit Test**
|
||||
|
||||
Zuerst der Test, `app-desktop/src/test/kotlin/kst4contest/view/compose/TypographyScalingTest.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
||||
class TypographyScalingTest {
|
||||
|
||||
@Test
|
||||
fun `the base size replaces the body size and the rest keeps its ratio`() {
|
||||
val original = Typography()
|
||||
val originalBody = original.bodyMedium.fontSize.value
|
||||
val originalTitle = original.titleMedium.fontSize.value
|
||||
|
||||
val scaled = original.scaledTo(originalBody * 2f)
|
||||
|
||||
assertEquals(originalBody * 2f, scaled.bodyMedium.fontSize.value, 0.01f)
|
||||
assertEquals(originalTitle * 2f, scaled.titleMedium.fontSize.value, 0.01f,
|
||||
"every style scales by the same factor, so the design keeps its proportions")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a base size of zero or less is ignored`() {
|
||||
val original = Typography()
|
||||
|
||||
assertEquals(original.bodyMedium.fontSize.value,
|
||||
original.scaledTo(0f).bodyMedium.fontSize.value, 0.01f)
|
||||
assertEquals(original.bodyMedium.fontSize.value,
|
||||
original.scaledTo(-5f).bodyMedium.fontSize.value, 0.01f)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.TypographyScalingTest'`
|
||||
Erwartet: FAIL — `scaledTo` existiert nicht.
|
||||
|
||||
Dann die Umsetzung, an `Theme.kt` angefügt:
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Scales every text style by the factor that turns the body size into
|
||||
* [targetBodySizeSp]. A value of zero or less is ignored, so a missing or
|
||||
* malformed configured size keeps the default design rather than collapsing it.
|
||||
*/
|
||||
internal fun Typography.scaledTo(targetBodySizeSp: Float): Typography {
|
||||
if (targetBodySizeSp <= 0f) {
|
||||
return this
|
||||
}
|
||||
val factor = targetBodySizeSp / bodyMedium.fontSize.value
|
||||
if (factor == 1f) {
|
||||
return this
|
||||
}
|
||||
fun TextStyle.scaled() = copy(fontSize = fontSize.value.times(factor).sp)
|
||||
return copy(
|
||||
displayLarge = displayLarge.scaled(), displayMedium = displayMedium.scaled(),
|
||||
displaySmall = displaySmall.scaled(), headlineLarge = headlineLarge.scaled(),
|
||||
headlineMedium = headlineMedium.scaled(), headlineSmall = headlineSmall.scaled(),
|
||||
titleLarge = titleLarge.scaled(), titleMedium = titleMedium.scaled(),
|
||||
titleSmall = titleSmall.scaled(), bodyLarge = bodyLarge.scaled(),
|
||||
bodyMedium = bodyMedium.scaled(), bodySmall = bodySmall.scaled(),
|
||||
labelLarge = labelLarge.scaled(), labelMedium = labelMedium.scaled(),
|
||||
labelSmall = labelSmall.scaled(),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Die Importe `androidx.compose.material3.Typography`, `androidx.compose.ui.text.TextStyle` und `androidx.compose.ui.unit.sp` ergänzen.
|
||||
|
||||
- [ ] **Schritt 4: Test laufen lassen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.TypographyScalingTest'`
|
||||
Erwartet: PASS, 2 Testfälle.
|
||||
|
||||
- [ ] **Schritt 5: Gesamtlauf**
|
||||
|
||||
Ausführen: `./gradlew clean build`
|
||||
Erwartet: BUILD SUCCESSFUL, **259** Testfälle, 0 Fehler.
|
||||
|
||||
- [ ] **Schritt 6: Größenzuwachs festhalten**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:collectRuntime && du -sh app-desktop/build/dist-libs`
|
||||
|
||||
Den Wert im Ledger notieren. Vor Compose liegt er bei etwa 60 MB; Skiko bringt plattformabhängige Bibliotheken mit. Aufgabe 5 vergleicht die Paketgröße dagegen.
|
||||
|
||||
- [ ] **Schritt 7: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Compile Compose in the desktop module and carry the configured font size into it"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 3: Machbarkeitsprobe — Compose-Fenster neben JavaFX
|
||||
|
||||
Deckt Prüfschwerpunkt 1 ab. **Diese Aufgabe entscheidet, ob die Etappenfolge 3b bis 5 tragfähig ist.**
|
||||
|
||||
**Dateien:**
|
||||
- Anlegen: `app-desktop/src/main/kotlin/kst4contest/view/compose/CoexistenceProbe.kt` — **Wegwerfcode**, wird in Schritt 6 gelöscht
|
||||
|
||||
**Schnittstellen:**
|
||||
- Erzeugt: nichts Dauerhaftes. Das Ergebnis ist eine Antwort, kein Code.
|
||||
|
||||
Compose Desktop zeichnet über Skiko in ein AWT-Fenster und betreibt einen eigenen Ereignis-Thread. JavaFX betreibt seinen. Beide in einem Prozess zu haben, ist nicht dasselbe wie beide einzeln zu haben.
|
||||
|
||||
- [ ] **Schritt 1: Die Probe schreiben**
|
||||
|
||||
`app-desktop/src/main/kotlin/kst4contest/view/compose/CoexistenceProbe.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import javafx.application.Platform
|
||||
|
||||
/**
|
||||
* THROWAWAY. Answers one question: can a Compose window open while a JavaFX
|
||||
* toolkit is running in the same process, with both staying responsive?
|
||||
*
|
||||
* Deleted once the answer is recorded.
|
||||
*/
|
||||
object CoexistenceProbe {
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
Platform.startup {
|
||||
println("[probe] JavaFX toolkit started on " + Thread.currentThread().name)
|
||||
}
|
||||
|
||||
// A heartbeat on the JavaFX thread. If Compose blocks it, this stops.
|
||||
val fxTicks = java.util.concurrent.atomic.AtomicInteger()
|
||||
val timer = java.util.Timer(true)
|
||||
timer.scheduleAtFixedRate(object : java.util.TimerTask() {
|
||||
override fun run() {
|
||||
Platform.runLater { fxTicks.incrementAndGet() }
|
||||
}
|
||||
}, 0L, 250L)
|
||||
|
||||
Thread {
|
||||
Thread.sleep(8000)
|
||||
println("[probe] fx ticks after 8 s: " + fxTicks.get() + " (expect roughly 32)")
|
||||
println("[probe] compose window still up: see whether it repaints")
|
||||
}.start()
|
||||
|
||||
application {
|
||||
Window(onCloseRequest = ::exitApplication, title = "Coexistence probe") {
|
||||
Text("If this window paints and the tick count keeps rising, both toolkits live.")
|
||||
}
|
||||
}
|
||||
println("[probe] compose application() returned; fx ticks: " + fxTicks.get())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: Die Probe laufen lassen**
|
||||
|
||||
Ausführen:
|
||||
```bash
|
||||
./gradlew :app-desktop:collectRuntime -q
|
||||
java -cp "app-desktop/build/dist-libs/*" kst4contest.view.compose.CoexistenceProbe 2>&1 | tee /tmp/probe.log
|
||||
```
|
||||
|
||||
Erwartet: Ein Fenster erscheint mit dem Text. Nach acht Sekunden erscheint `fx ticks after 8 s:` mit einem Wert **um 32**.
|
||||
|
||||
Bewertung:
|
||||
- **Fenster erscheint und Ticks steigen** → beide Toolkits koexistieren. Weiter mit Schritt 3.
|
||||
- **Fenster erscheint, Ticks bleiben bei 0 oder stehen** → Compose blockiert den JavaFX-Thread. Weiter mit Schritt 4.
|
||||
- **Kein Fenster, Ausnahme oder Absturz** → Weiter mit Schritt 4.
|
||||
|
||||
- [ ] **Schritt 3: Die umgekehrte Richtung prüfen**
|
||||
|
||||
Der eigentliche Fall ist nicht „Compose startet JavaFX", sondern „die laufende JavaFX-Anwendung öffnet ein Compose-Fenster". Die Probe umbauen: `Platform.startup` behalten, eine JavaFX-`Stage` zeigen, und **aus einem JavaFX-Knopfdruck heraus** das Compose-Fenster öffnen.
|
||||
|
||||
`application { }` blockiert den aufrufenden Thread bis zum Schließen. Aus dem JavaFX-Thread darf das deshalb **nicht** direkt aufgerufen werden. Zu prüfen ist, ob ein eigener Thread mit `application { }` funktioniert, oder ob die `ComposeWindow`-Schnittstelle ohne `application { }` der richtige Weg ist.
|
||||
|
||||
Erwartet: Nach dem Knopfdruck erscheint das Compose-Fenster, das JavaFX-Fenster bleibt bedienbar — Knopf drückbar, Fenster verschiebbar —, und die Tickzahl steigt weiter.
|
||||
|
||||
Das Ergebnis samt des benutzten Wegs (`application { }` im eigenen Thread, oder `ComposeWindow` direkt) im Ledger festhalten. **Plan 3b baut darauf auf.**
|
||||
|
||||
- [ ] **Schritt 4: Wenn die Probe scheitert**
|
||||
|
||||
Dann endet dieser Plan hier. Im Ledger festhalten, was genau passiert ist, und berichten:
|
||||
|
||||
> Die Etappenfolge im Spec setzt voraus, dass Fenster einzeln umgestellt werden können. Wenn Compose und JavaFX nicht koexistieren, ist das nicht möglich, und es bleibt nur ein Umschalttag, an dem die gesamte Oberfläche wechselt. Das ist eine Änderung am Spec, nicht am Plan, und gehört vor jeder weiteren Arbeit entschieden.
|
||||
|
||||
Aufgabe 4 bis 7 **nicht** ausführen.
|
||||
|
||||
- [ ] **Schritt 5: Das Ergebnis festhalten**
|
||||
|
||||
Ins Ledger, wörtlich messbar: erscheint das Fenster, bleibt JavaFX bedienbar, wie viele Ticks nach acht Sekunden, welcher Weg funktioniert hat.
|
||||
|
||||
- [ ] **Schritt 6: Die Probe löschen**
|
||||
|
||||
```bash
|
||||
git rm app-desktop/src/main/kotlin/kst4contest/view/compose/CoexistenceProbe.kt
|
||||
```
|
||||
|
||||
Eine Probe ist eine Antwort, kein Bestandteil. Die Antwort steht im Ledger.
|
||||
|
||||
- [ ] **Schritt 7: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Record the Compose/JavaFX coexistence probe result"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 4: Der Profil-Auswahldialog in Compose
|
||||
|
||||
**Dateien:**
|
||||
- Anlegen: `app-desktop/src/main/kotlin/kst4contest/view/compose/OperatorProfilePickerWindow.kt`
|
||||
- Ändern: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java` (`:6546` als Methodenverweis `OperatorProfilePickerDialog::showAndSelect`, `:6612` als direkter Aufruf)
|
||||
- Löschen: `app-desktop/src/main/java/kst4contest/view/OperatorProfilePickerDialog.java` (145 Zeilen)
|
||||
- Test: `app-desktop/src/test/kotlin/kst4contest/view/compose/OperatorProfilePickerStateTest.kt`
|
||||
|
||||
**Schnittstellen:**
|
||||
- Konsumiert: `Kst4ContestTheme` aus Aufgabe 2, das Ergebnis von Aufgabe 3.
|
||||
- Erzeugt: `OperatorProfilePickerWindow.showAndSelect(profiles, preselectedId)` mit **derselben Signatur und demselben Rückgabetyp** wie `OperatorProfilePickerDialog.showAndSelect`. Der Name bleibt, weil `Kst4ContestApplication:6546` ihn als Methodenverweis `::showAndSelect` benutzt — eine abweichende Signatur bricht dort nicht sichtbar, sondern erst beim Übersetzen an einer unerwarteten Stelle.
|
||||
|
||||
Dieses Fenster ist mit Bedacht gewählt: 145 Zeilen, in sich abgeschlossen, und es läuft **vor** dem Aufbau der Hauptoberfläche. Es ist damit der einzige Ort, an dem ein Compose-Fenster ohne gleichzeitig offene JavaFX-Fenster steht — der einfachste mögliche Anfang.
|
||||
|
||||
- [ ] **Schritt 1: Den bisherigen Vertrag ablesen**
|
||||
|
||||
Ausführen:
|
||||
```bash
|
||||
sed -n '1,60p' app-desktop/src/main/java/kst4contest/view/OperatorProfilePickerDialog.java
|
||||
grep -rn "OperatorProfilePickerDialog" app-desktop/src core/src
|
||||
```
|
||||
|
||||
Notieren: Signatur, Rückgabewert bei Abbruch, und wer den Dialog aufruft. Der neue Weg muss **denselben** Vertrag erfüllen, insbesondere bei Abbruch: laut `PROJECT_CONTEXT.md` fragt ein Start mit keinem oder genau einem Profil gar nichts und schreibt nichts.
|
||||
|
||||
- [ ] **Schritt 2: Den Zustand von der Darstellung trennen, mit Test**
|
||||
|
||||
Die Auswahllogik gehört in eine testbare Klasse ohne Compose-Bezug. Zuerst der Test:
|
||||
|
||||
`app-desktop/src/test/kotlin/kst4contest/view/compose/OperatorProfilePickerStateTest.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
|
||||
class OperatorProfilePickerStateTest {
|
||||
|
||||
@Test
|
||||
fun `the preselected profile is selected when it is in the list`() {
|
||||
val state = OperatorProfilePickerState(listOf("root", "contest"), preselectedId = "contest")
|
||||
assertEquals("contest", state.selectedId)
|
||||
assertTrue(state.canConfirm)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown preselection falls back to the first profile`() {
|
||||
val state = OperatorProfilePickerState(listOf("root", "contest"), preselectedId = "gone")
|
||||
assertEquals("root", state.selectedId,
|
||||
"a profile that disappeared must not leave the dialog unusable")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an empty list cannot be confirmed`() {
|
||||
val state = OperatorProfilePickerState(emptyList(), preselectedId = null)
|
||||
assertNull(state.selectedId)
|
||||
assertFalse(state.canConfirm)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting changes the selection`() {
|
||||
val state = OperatorProfilePickerState(listOf("root", "contest"), preselectedId = "root")
|
||||
state.select("contest")
|
||||
assertEquals("contest", state.selectedId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting something absent is ignored`() {
|
||||
val state = OperatorProfilePickerState(listOf("root"), preselectedId = "root")
|
||||
state.select("nope")
|
||||
assertEquals("root", state.selectedId, "an unknown id must not clear the selection")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.OperatorProfilePickerStateTest'`
|
||||
Erwartet: FAIL — `OperatorProfilePickerState` existiert nicht.
|
||||
|
||||
- [ ] **Schritt 3: Den Zustand schreiben**
|
||||
|
||||
In `OperatorProfilePickerWindow.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
/**
|
||||
* Selection state of the operator profile picker, free of Compose so the rules
|
||||
* are testable without a toolkit.
|
||||
*/
|
||||
class OperatorProfilePickerState(
|
||||
val profileIds: List<String>,
|
||||
preselectedId: String?,
|
||||
) {
|
||||
var selectedId: String? = preselectedId.takeIf { profileIds.contains(it) } ?: profileIds.firstOrNull()
|
||||
private set
|
||||
|
||||
val canConfirm: Boolean
|
||||
get() = selectedId != null
|
||||
|
||||
/** Ignores an id that is not in the list: a vanished profile must not clear the selection. */
|
||||
fun select(id: String) {
|
||||
if (profileIds.contains(id)) {
|
||||
selectedId = id
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: Test laufen lassen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.OperatorProfilePickerStateTest'`
|
||||
Erwartet: PASS, 5 Testfälle.
|
||||
|
||||
- [ ] **Schritt 5: Das Fenster schreiben**
|
||||
|
||||
Den in Aufgabe 3 Schritt 3 ermittelten Weg benutzen. Der Aufruf ist blockierend und liefert die Wahl oder `null` bei Abbruch — dieselbe Form wie bisher, damit `OperatorProfileChoiceRequester` sich nicht ändert.
|
||||
|
||||
Das Fenster nutzt `Kst4ContestTheme` mit dem konfigurierten Dunkelmodus und der Basis-Schriftgröße. Woher beide zu diesem frühen Zeitpunkt kommen, ist zu klären: der Dialog läuft, bevor ein Profil gewählt ist, also bevor dessen `preferences.xml` gelesen wurde. Zu benutzen sind die Werte des Wurzelprofils; ist auch das nicht lesbar, die Vorgaben. Diese Entscheidung als `Ruling:` festhalten.
|
||||
|
||||
- [ ] **Schritt 6: Den Aufrufer umstellen und den alten Dialog löschen**
|
||||
|
||||
```bash
|
||||
git rm app-desktop/src/main/java/kst4contest/view/OperatorProfilePickerDialog.java
|
||||
```
|
||||
|
||||
Beide Aufrufstellen in `Kst4ContestApplication` umstellen: den Methodenverweis `:6546` und den direkten Aufruf `:6612`. Bleibt die Signatur gleich, genügt der Austausch des Klassennamens.
|
||||
|
||||
Ausführen: `grep -rn "OperatorProfilePickerDialog" app-desktop/src core/src`
|
||||
Erwartet: keine Ausgabe.
|
||||
|
||||
- [ ] **Schritt 7: Übersetzen und testen**
|
||||
|
||||
Ausführen: `./gradlew clean build`
|
||||
Erwartet: BUILD SUCCESSFUL, **264** Testfälle, 0 Fehler.
|
||||
|
||||
- [ ] **Schritt 8: Den Dialog wirklich sehen**
|
||||
|
||||
Der Dialog erscheint nur, wenn mehr als ein Operator-Profil vorhanden ist. Ein zweites anlegen, falls nötig — im laufenden Programm über **Einstellungen → Profiles**, damit `profiles.xml` auf dem vorgesehenen Weg entsteht und nicht von Hand.
|
||||
|
||||
Dann: `./gradlew :app-desktop:run`
|
||||
|
||||
Erwartet: Das Compose-Fenster erscheint, listet beide Profile, die Vorauswahl steht auf dem zuletzt benutzten. Auswählen und bestätigen startet die Anwendung mit diesem Profil. Abbrechen verhält sich wie zuvor.
|
||||
|
||||
- [ ] **Schritt 9: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Pick the operator profile in a Compose window"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 5: Packaging auf `compose.desktop.nativeDistributions`
|
||||
|
||||
Deckt Prüfschwerpunkt 3 ab.
|
||||
|
||||
**Dateien:**
|
||||
- Ändern: `app-desktop/build.gradle.kts`
|
||||
- Ändern: `gradle.properties`
|
||||
- Ändern: `.github/workflows/nightly-artifacts.yml`, `.github/workflows/tagged-release.yml`, `.github/workflows/pr-compile-check.yml`
|
||||
- Ändern: `packaging/macos/build-signed-dmg.sh`, `packaging/aur/kst4contest/PKGBUILD`, `packaging/aur/kst4contest-git/PKGBUILD`
|
||||
|
||||
**Schnittstellen:**
|
||||
- Konsumiert: nichts.
|
||||
- Erzeugt: die Gradle-Aufgaben des Compose-Plugins (`packageDistributionForCurrentOS`, `createDistributable`) an der Stelle von `packageImage`.
|
||||
|
||||
Etappe 1 hat die Modulliste in `gradle.properties` unter `jpackageAddModules` hinterlegt, weil `AddModules.java` entfiel. Zwei Einträge darin wurden **teuer erkauft**: `jdk.jsobject` für die Karten-Brücke und `jdk.unsupported`, ohne das der Marlin-Renderer an `sun/misc/Unsafe` stirbt. Das Compose-Plugin ermittelt Module selbst — deshalb ist die erste Frage dieser Aufgabe, ob beide noch enthalten sind.
|
||||
|
||||
- [ ] **Schritt 1: Die heutigen Werte festhalten**
|
||||
|
||||
Ausführen:
|
||||
```bash
|
||||
grep -n "^jpackageAddModules" gradle.properties
|
||||
./gradlew :app-desktop:packageImage -q
|
||||
du -sh app-desktop/build/jpackage/praktiKST
|
||||
grep -o "MODULES=.*" app-desktop/build/jpackage/praktiKST/lib/runtime/release | tr ' ' '\n' | sort > /tmp/module-liste-vorher.txt
|
||||
wc -l < /tmp/module-liste-vorher.txt
|
||||
```
|
||||
|
||||
Paketgröße und Modulzahl im Ledger notieren. Sie sind der Vergleichsmaßstab.
|
||||
|
||||
- [ ] **Schritt 2: `nativeDistributions` einrichten**
|
||||
|
||||
An `app-desktop/build.gradle.kts` anfügen:
|
||||
|
||||
```kotlin
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "kst4contest.view.Main"
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.AppImage,
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb,
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Rpm,
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi,
|
||||
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg,
|
||||
)
|
||||
packageName = "praktiKST"
|
||||
packageVersion = providers.gradleProperty("composePackageVersion").get()
|
||||
|
||||
/*
|
||||
* Kept from the former module-info.java, minus the javafx.* modules.
|
||||
* jdk.jsobject carries netscape.javascript for the map bridge, and
|
||||
* jdk.unsupported carries sun.misc.Unsafe, without which the JavaFX
|
||||
* Marlin renderer fails to start. Neither is inferred: JavaFX comes
|
||||
* from the classpath, so nothing declares them.
|
||||
*/
|
||||
modules(
|
||||
"java.desktop", "java.net.http", "java.sql",
|
||||
"jdk.crypto.ec", "jdk.jsobject", "jdk.net",
|
||||
"jdk.xml.dom", "jdk.unsupported",
|
||||
)
|
||||
|
||||
linux { iconFile.set(rootProject.file("packaging/icons/kst4contest.png")) }
|
||||
windows { iconFile.set(rootProject.file("packaging/icons/kst4contest.ico")) }
|
||||
macOS {
|
||||
iconFile.set(rootProject.file("packaging/icons/kst4contest.icns"))
|
||||
bundleID = "de.x08.KST4Contest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `mainClass` ist `kst4contest.view.Main`, nicht `Kst4ContestApplication`. Der Grund steht in Etappe 1: eine `Application`-Unterklasse startet vom Klassenpfad nicht.
|
||||
|
||||
> `bundleID` muss `de.x08.KST4Contest` bleiben — `packaging/macos/build-signed-dmg.sh` signiert dagegen, und ein geänderter Wert bricht die Signatur.
|
||||
|
||||
- [ ] **Schritt 3: Die Paketversion bereitstellen**
|
||||
|
||||
`nativeDistributions` verlangt eine Version nach den Regeln des Zielformats: MSI und DMG akzeptieren **kein** `1.50.0-nightly`, sondern nur `MAJOR.MINOR.PATCH` mit Zahlen.
|
||||
|
||||
In `gradle.properties` ergänzen:
|
||||
|
||||
```properties
|
||||
# nativeDistributions requires MAJOR.MINOR.PATCH with digits only. MSI and DMG
|
||||
# reject a qualifier, so the nightly suffix from `version` cannot be used here.
|
||||
composePackageVersion=1.50.0
|
||||
```
|
||||
|
||||
Ausführen: `grep -n "^version=\|^composePackageVersion=" gradle.properties`
|
||||
Erwartet: beide Zeilen; `version` behält den Zusatz `-nightly`, `composePackageVersion` nicht.
|
||||
|
||||
- [ ] **Schritt 4: Lokal paketieren und vergleichen**
|
||||
|
||||
Ausführen:
|
||||
```bash
|
||||
./gradlew :app-desktop:createDistributable -q
|
||||
find app-desktop/build/compose/binaries -maxdepth 4 -name "praktiKST" -type d
|
||||
```
|
||||
|
||||
Die Modulliste des neuen Runtime gegen die alte prüfen:
|
||||
```bash
|
||||
NEW=$(find app-desktop/build/compose/binaries -name release -path "*runtime*" | head -1)
|
||||
grep -o "MODULES=.*" "$NEW" | tr ' ' '\n' | sort > /tmp/module-liste-nachher.txt
|
||||
diff /tmp/module-liste-vorher.txt /tmp/module-liste-nachher.txt
|
||||
```
|
||||
|
||||
Erwartet: `jdk.jsobject` und `jdk.unsupported` sind **enthalten**. Fehlt einer, ist die `modules(...)`-Angabe aus Schritt 2 nicht angekommen — nicht weitergehen, bis sie drin ist.
|
||||
|
||||
- [ ] **Schritt 5: Das Paket starten**
|
||||
|
||||
Ausführen: den Launcher unter `app-desktop/build/compose/binaries/main/app/praktiKST/bin/praktiKST`
|
||||
|
||||
Erwartet: Das Fenster erscheint. Im Protokoll **kein** `NoClassDefFoundError: sun/misc/Unsafe`, **kein** Fehler zu `JSObject`. Das Kartenfenster öffnen und Kacheln sehen — das ist die Prüfung auf `jdk.jsobject`.
|
||||
|
||||
- [ ] **Schritt 6: Größe vergleichen**
|
||||
|
||||
Ausführen: `du -sh app-desktop/build/compose/binaries/main/app/praktiKST`
|
||||
|
||||
Gegen den Wert aus Schritt 1 stellen und beide ins Ledger. Ein Zuwachs ist zu erwarten: Skiko bringt eigene Bibliotheken mit. Wächst das Paket um mehr als etwa 100 MB, im Ledger festhalten und berichten — die Download-Größe ist für Anwender im Feld ein echtes Thema.
|
||||
|
||||
- [ ] **Schritt 7: Die CI umstellen**
|
||||
|
||||
In den Workflows die Aufrufe von `:app-desktop:collectRuntime` plus dem folgenden `jpackage`-Block ersetzen durch die passende Compose-Aufgabe:
|
||||
|
||||
| Job | Aufgabe |
|
||||
|---|---|
|
||||
| AppImage | `:app-desktop:createDistributable`, dann das bestehende AppDir-Verfahren auf dem neuen Pfad |
|
||||
| Deb | `:app-desktop:packageDeb` |
|
||||
| Rpm | `:app-desktop:packageRpm` |
|
||||
| Windows ZIP | `:app-desktop:createDistributable`, dann ZIP wie bisher |
|
||||
| macOS DMG | `:app-desktop:createDistributable`, danach `build-signed-dmg.sh` |
|
||||
| Arch, Flatpak | `:app-desktop:createDistributable` |
|
||||
|
||||
Der Ausgabepfad wechselt von `app-desktop/build/jpackage/praktiKST` auf `app-desktop/build/compose/binaries/main/app/praktiKST`. Vor dem Bearbeiten den echten Pfad aus Schritt 4 ablesen und nicht aus diesem Plan übernehmen — er hängt von der Plugin-Fassung ab.
|
||||
|
||||
`--add-modules`-Argumente und die `ADD_MODULES`-Zeilen entfallen in der CI: die Modulliste steht jetzt in `build.gradle.kts`. `jpackageAddModules` in `gradle.properties` wird gelöscht, **nachdem** alle Verwendungen weg sind.
|
||||
|
||||
Ausführen: `grep -rn "jpackageAddModules\|collectRuntime\|--add-modules" .github/ packaging/`
|
||||
Erwartet: keine Ausgabe.
|
||||
|
||||
- [ ] **Schritt 8: Die Linux-Jobs lokal prüfen**
|
||||
|
||||
Wie in Etappe 1, mit `act`:
|
||||
|
||||
```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 --pull=false -j build-flatpak -W .github/workflows/nightly-artifacts.yml
|
||||
```
|
||||
|
||||
Erwartet: Alle Bauschritte erfolgreich. Der jeweils letzte Schritt — Artefakt-Upload beziehungsweise Flatpak-Signierschlüssel — scheitert an `act`, nicht am Build; das war in Etappe 1 genauso.
|
||||
|
||||
- [ ] **Schritt 9: Windows und macOS**
|
||||
|
||||
Beide sind lokal nur eingeschränkt prüfbar. Der Weg aus Etappe 1 gilt weiter: WinBoat für Windows, ein erreichbarer Mac für macOS. Die Modulprüfung aus Schritt 4 und der Start aus Schritt 5 sind auf **jeder** Plattform zu wiederholen — `jdk.unsupported` fiel in Etappe 1 erst beim Start auf, nicht beim Bauen.
|
||||
|
||||
Die **MSI-Strecke mit WiX** ist bis heute nie gelaufen. `packageMsi` ist neu und hier der Punkt mit dem größten Restrisiko.
|
||||
|
||||
- [ ] **Schritt 10: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Package through compose.desktop.nativeDistributions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 6: Lebensdauer, Beenden und Erscheinungsbild
|
||||
|
||||
Deckt die Prüfschwerpunkte 2, 4 und 5 ab.
|
||||
|
||||
**Dateien:**
|
||||
- Ändern: `app-desktop/src/main/java/kst4contest/view/ApplicationRuntimeLauncher.java`, falls nötig
|
||||
- Ändern: `app-desktop/src/main/kotlin/kst4contest/view/compose/OperatorProfilePickerWindow.kt`, falls nötig
|
||||
|
||||
- [ ] **Schritt 1: Beenden nach dem Profil-Dialog prüfen**
|
||||
|
||||
`Platform.setImplicitExit(false)` hält den Prozess am Leben, wenn alle JavaFX-Fenster zu sind; alle Ausgänge laufen über `ApplicationRuntimeLauncher.exitApplication()`. Ein AWT-Fenster von Compose bringt eine zweite, unabhängige Ursache für einen laufenden Prozess mit.
|
||||
|
||||
Ausführen: Anwendung starten, Profil wählen, Anwendung über das Fenster-Kreuz schließen.
|
||||
|
||||
```bash
|
||||
pgrep -f Kst4ContestApplication || echo "Prozess beendet"
|
||||
```
|
||||
Erwartet: `Prozess beendet`.
|
||||
|
||||
Dasselbe für den Abbruch im Profil-Dialog: Dialog abbrechen, dann prüfen.
|
||||
Erwartet: `Prozess beendet`.
|
||||
|
||||
Bleibt der Prozess stehen, ist es ein Nicht-Daemon-Thread von AWT. Im Ledger festhalten, welcher — `jstack` zeigt es — und den Compose-Fensterweg so ändern, dass er beim Schließen aufräumt.
|
||||
|
||||
- [ ] **Schritt 2: Profilwechsel im laufenden Betrieb**
|
||||
|
||||
Ausführen: Anwendung starten, **Einstellungen → Profiles**, auf ein anderes Profil wechseln.
|
||||
|
||||
Erwartet: Der Wechsel funktioniert wie vor dieser Etappe. Die Stationsliste des neuen Profils füllt sich, keine doppelten Listener, der Prozess läuft weiter. Laut `PROJECT_CONTEXT.md` reißt ein Wechsel die Laufzeit ab und baut eine neue `Kst4ContestApplication`; das Compose-Fenster des Dialogs darf davon nicht betroffen sein, weil es zu diesem Zeitpunkt längst geschlossen ist.
|
||||
|
||||
- [ ] **Schritt 3: Erscheinungsbild vergleichen**
|
||||
|
||||
Zwei Profile mit **unterschiedlicher** Basis-Schriftgröße anlegen, dann jeweils starten.
|
||||
|
||||
Erwartet: Die Schrift im Compose-Dialog folgt der Größe des Wurzelprofils (die Entscheidung aus Aufgabe 4 Schritt 5) und wirkt neben den JavaFX-Fenstern nicht auffällig größer oder kleiner. Der Dunkelmodus stimmt mit der Vorgabe überein.
|
||||
|
||||
Weicht es sichtbar ab, ist das ein Befund für Plan 3b — dort entsteht das Einstellungsfenster, das die Werte kennt. Im Ledger festhalten, nicht hier reparieren.
|
||||
|
||||
- [ ] **Schritt 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Keep process lifetime and appearance intact with a Compose window in the mix"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abnahme der Etappe 3a
|
||||
|
||||
- [ ] `./gradlew clean build` läuft durch, mindestens **264** Testfälle, 0 Fehler.
|
||||
- [ ] `grep -rn "javafx" core/src` liefert keine Ausgabe — `core` blieb unberührt.
|
||||
- [ ] Das Ergebnis der Koexistenzprobe steht im Ledger, mit dem benutzten Weg.
|
||||
- [ ] Der Profil-Auswahldialog ist in Compose, `OperatorProfilePickerDialog.java` existiert nicht mehr.
|
||||
- [ ] `createDistributable` erzeugt ein startfähiges Paket, dessen Runtime `jdk.jsobject` und `jdk.unsupported` enthält.
|
||||
- [ ] Das Kartenfenster zeigt im gepackten Stand Kacheln — die Prüfung auf `jdk.jsobject`.
|
||||
- [ ] Alle fünf Linux-Jobs laufen unter `act` bis zum Upload-Schritt durch.
|
||||
- [ ] Der Prozess endet nach dem Schließen und nach einem Abbruch im Profil-Dialog.
|
||||
- [ ] Der Profilwechsel im laufenden Betrieb funktioniert unverändert.
|
||||
- [ ] Paketgröße vorher und nachher stehen im Ledger.
|
||||
|
||||
## Was danach kommt
|
||||
|
||||
**Plan 3b: `DataTable` und Einstellungsfenster.** Er wird geschrieben, wenn dieser Plan durch ist — und zwar aus zwei Gründen erst dann. Der Zuschnitt der `DataTable` hängt am Ergebnis der Koexistenzprobe, und die Erfahrung aus Etappe 2 gehört hinein:
|
||||
|
||||
- Ein Spiegel, der die ganze Liste ersetzt, kostet Auswahl und Scrollposition. In Compose ist der Hebel dagegen ein **stabiler Schlüssel** je Zeile, nicht ein Gleichheitsvergleich.
|
||||
- Sortierung wird aus der kanonischen Liste abgeleitet, nie aus dem vorher Angezeigten.
|
||||
- Fokus ist ausdrücklich zu führen. Zwei der drei Fehler aus Etappe 2 waren Fokusfehler, und keiner war durch Lesen zu finden.
|
||||
- Ein Sammelvorgang ist **eine** Zustandsänderung, nicht eine je Element.
|
||||
@@ -1,852 +0,0 @@
|
||||
# Etappe 3b — `DataTable` und Einstellungsfenster: 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:** Das Einstellungsfenster mit seinen zwölf Reitern ist in Compose, funktionsgleich, und schreibt unverändert dasselbe `preferences.xml`.
|
||||
|
||||
**Architektur:** Ein Compose-Fenster ersetzt `settingsStage`. Jeder Reiter bekommt eine Zustandsklasse ohne Compose-Bezug — eine **typisierte Fassade**, die jede Eigenschaft direkt aus `ChatPreferences` liest und **sofort** dorthin zurückschreibt. Eine `DataTable`-Komponente bedient die drei einspaltigen, editierbaren Listen des Fensters.
|
||||
|
||||
**Tech-Stack:** Kotlin 2.2.0, Compose Multiplatform 1.8.2, Gradle 9.7.1, Java 21.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-25-compose-migration-design.md`
|
||||
|
||||
## Korrektur am Plan: es gibt kein Bestätigen, nur Durchschreiben
|
||||
|
||||
Dieser Plan schrieb zunächst vor, jeder Reiter solle aus den Einstellungen lesen und **nur bei `apply()`** zurückschreiben, mit der Begründung, unbestätigte Werte dürften nicht in `preferences.xml` landen. **Am Code geprüft ist das falsch**, und zwar nicht im Detail, sondern im Modell:
|
||||
|
||||
- **„Save settings"** (`Kst4ContestApplication:12523`) ruft ausschliesslich `writePreferencesToXmlFile()` und bricht einen anstehenden Layout-Autosave ab. Es **sammelt keine Werte ein**.
|
||||
- **„Apply/Close prefs"** (`:12382`) ruft `settingsStage.hide()`. Nichts weiter.
|
||||
|
||||
Jedes Bedienelement des JavaFX-Fensters schreibt also **beim Ändern sofort** in das `ChatPreferences`-Objekt im Speicher; die beiden Knöpfe schreiben nur auf die Platte beziehungsweise schliessen. Was `PROJECT_CONTEXT` verbietet, ist etwas anderes: dass **selektive Layout-Schreibvorgänge** unbestätigte funktionale Einstellungen mitpersistieren. Das betrifft den Autosave, nicht die Bedienelemente.
|
||||
|
||||
**Folge:** Eine Zustandsklasse mit Puffer und `apply()` wäre eine Verhaltensänderung, auf die sich ein Operator verlassen kann — heute wirkt eine geänderte Einstellung sofort in der laufenden Sitzung, auch wenn sie nie auf die Platte kommt. Die Zustandsklassen sind deshalb **durchschreibende Fassaden ohne `apply()`**:
|
||||
|
||||
```kotlin
|
||||
var stn_loginCallSign: String
|
||||
get() = prefs.getStn_loginCallSign()
|
||||
set(value) { prefs.setStn_loginCallSign(value) }
|
||||
```
|
||||
|
||||
Das macht sie zugleich einfacher: kein Puffer, kein Abgleich, keine Frage wann geschrieben wird.
|
||||
|
||||
## Das zweite Netz: Schreibstellen ausserhalb der Einstellungen
|
||||
|
||||
`SettingsFieldCoverageTest` prüft nur `ChatPreferences`-Setter. Eine Schreibstelle auf einem anderen Objekt fällt durch — und das JavaFX-Fenster hat **fünf** davon, mechanisch ermittelt: die Setter im Bereich, die `ChatPreferences` nicht deklariert, geschnitten mit den Settern der eigenen Projektklassen.
|
||||
|
||||
| Setter | Klasse | Wofür |
|
||||
|---|---|---|
|
||||
| `setChatCategoryMain` | `ChatController` | bei jeder Kategorieauswahl, **zusätzlich** zum Einstellungs-Setter |
|
||||
| `setChatCategorySecondChat` | `ChatController` | dito; `null` bedeutet „kein zweiter Chat" |
|
||||
| `setCallSign`, `setFrequency`, `setQra` | `ChatMember` | das eigene Stationsobjekt, im Gleichschritt mit den Stationsfeldern |
|
||||
|
||||
Fällt der `ChatController`-Teil weg, bleibt die laufende Sitzung auf der alten Kategorie, während die gespeicherte Einstellung etwas anderes sagt — und **kein Test des ersten Netzes würde das merken**. `SettingsForeignWriteCoverageTest` deckt es ab.
|
||||
|
||||
Weil eine Zustandsklasse dafür einen `ChatController` bräuchte und damit im Test eine Datenbank öffnen würde, nimmt sie stattdessen **Rückrufe** entgegen:
|
||||
|
||||
```kotlin
|
||||
class StationTabState(
|
||||
private val prefs: ChatPreferences,
|
||||
private val applyMainCategory: (ChatCategory?) -> Unit,
|
||||
private val applySecondCategory: (ChatCategory?) -> Unit,
|
||||
)
|
||||
```
|
||||
|
||||
## Korrektur am Spec: die `DataTable` wird hier nicht validiert
|
||||
|
||||
Das Spec nennt die `DataTable` „den wichtigsten einzelnen Baustein" und erwartet, dass das Einstellungsfenster sie belastet: *„Wird diese Komponente gut, ist der Rest Fleißarbeit."* Gemessen am Code trägt das nicht.
|
||||
|
||||
Die sieben Tabellen mit gespeicherten Spaltenbreiten sitzen **alle** in den Hauptfenstern:
|
||||
|
||||
| Tabelle | `TableLayoutManager`-ID |
|
||||
|---|---|
|
||||
| `tbl_chatMemberTable` | `chat-members` |
|
||||
| `tbl_generalMSGTable` | `public-messages` |
|
||||
| `tbl_privateMSGTable` | `private-messages` |
|
||||
| `tbl_toOtherMSGTable` | — |
|
||||
| `tbl_furtherInfoAbtCallsignMSGTable` | `selected-station-messages` |
|
||||
| `tbl_DXCTable` | `dx-cluster-main` / `dx-cluster-monitor` |
|
||||
| `tbl_chatMemberWkdDBTable` | `worked-database` |
|
||||
|
||||
Die drei Tabellen im Einstellungsfenster — `tblVw_shortcuts`, `tblVw_textsnippets`, `tblVw_notify_sniffCallSigns` — sind **einspaltige, editierbare Zeichenkettenlisten** mit `TextFieldTableCell` und `setOnEditCommit`. Sie haben **keine** gespeicherten Breiten (null `install`-Aufrufe), keine Sortierung, kein Zell-Styling, keine mehrspaltige Struktur.
|
||||
|
||||
**Folge für diesen Plan:** Die `DataTable` wird hier für genau das gebaut, was diese drei Listen brauchen, mit einer Schnittstelle, die die Anforderungen der Hauptfenster später aufnehmen kann, ohne umgebaut zu werden. Spaltenbreiten mit stabilen IDs, Sortierung und Zeilen-Styling entstehen in **Etappe 5**, wo ihre Verbraucher sitzen. Sie jetzt zu bauen hieße, blind zu entwerfen.
|
||||
|
||||
Das ist keine Verkleinerung des Ziels, sondern eine Verschiebung an die Stelle, an der sie prüfbar ist.
|
||||
|
||||
## Was aus Etappe 2 in die `DataTable` einfließt
|
||||
|
||||
Vier Dinge, die dort Fehler gekostet haben und hier von Anfang an richtig gehören:
|
||||
|
||||
1. **Schlüssel statt Gleichheit.** Der JavaFX-Spiegel ersetzte bei jeder Änderung die ganze Liste und kostete Auswahl und Scrollposition; dagegen half ein `equals`-Vergleich, der wiederum an `ChatMember.equals` als Überladung hing. In Compose ist der Hebel ein **stabiler Schlüssel je Zeile** (`key = { … }` in `LazyColumn`). Kein Gleichheitsvergleich auf Fachobjekten.
|
||||
2. **Sortierung leitet aus der kanonischen Liste ab**, nie aus dem vorher Angezeigten. `SortedList` fügte per Binärsuche ein und liess bei Gleichstand eine undefinierte Position — die neue Regel ist Ankunftsreihenfolge, festgeschrieben in `ChatMemberListViewTest`.
|
||||
3. **Fokus wird ausdrücklich geführt.** Zwei der drei Fehler aus Etappe 2 waren Fokusfehler, und keiner war durch Lesen zu finden. In Compose ist Fokus explizit (`FocusRequester`) — das ist eine Chance, keine Last.
|
||||
4. **Ein Sammelvorgang ist eine Zustandsänderung**, nicht eine je Element.
|
||||
|
||||
## Globale Randbedingungen
|
||||
|
||||
- **`preferences.xml` bleibt bitweise gleichwertig.** Version 7, dieselben Elementnamen, dieselbe Reihenfolge, unbekannte Knoten bleiben erhalten. Das ist die härteste Vorgabe dieses Plans und der Gegenstand der Abnahme.
|
||||
- **Kein Funktionsverlust.** Jede Einstellung, die das JavaFX-Fenster setzen konnte, muss das Compose-Fenster setzen können. Ein Reiter gilt erst als fertig, wenn seine Felder vollständig sind.
|
||||
- `core` wird **nicht** angefasst.
|
||||
- **`Platform.runLater` und `uiDispatcher.runOnUi` sind nicht dasselbe.** Der Dispatcher führt inline aus, wenn der Aufrufer schon auf dem UI-Thread ist. Siehe `prepareCqTextForCallsign`; das hat in Etappe 2 einen Fehler gekostet.
|
||||
- **`application(exitProcessOnExit = false)`** bei jedem Compose-Fenster. Die Vorgabe `true` beendet den Prozess, wenn das letzte Compose-Fenster schliesst — in Etappe 3a hätte das die Anwendung nach der Profilwahl beendet.
|
||||
- Kommentare und KDoc auf Englisch, Kommunikation auf Deutsch.
|
||||
- Die Invarianten aus dem Spec-Abschnitt „Invarianten" gelten unverändert.
|
||||
|
||||
## Prüfschwerpunkte
|
||||
|
||||
1. **Eine Einstellung verschwindet still.** 2341 Zeilen mit Hunderten Feldern: ein beim Portieren vergessenes Feld fällt nicht beim Übersetzen auf, sondern wenn ein Operator im Contest merkt, dass seine Einstellung weg ist. → Aufgabe 2 und die Abnahme.
|
||||
2. **`preferences.xml` verliert Knoten.** Selektives Schreiben muss unbekannte XML-Knoten erhalten; **Save Settings** bleibt der vollständige Schreiber. Ein Compose-Fenster, das die Datei neu aufbaut statt zu aktualisieren, löscht stillschweigend, was es nicht kennt. → Aufgabe 2.
|
||||
3. **Eine Liste verliert Einträge beim Bearbeiten.** Die drei Listen sind editierbar; ein Bearbeitungsvorgang, der den Roster ersetzt statt den Eintrag zu ändern, wirft angemeldete Zuhörer ab. In Etappe 2 war das der Grund, Setter auf `setAll` umzustellen statt das Feld zu tauschen. → Aufgabe 1.
|
||||
4. **Der Profilwechsel bricht.** Der Reiter **Profiles** löst einen Laufzeitwechsel aus, der die ganze `Kst4ContestApplication` neu baut — aus einem Compose-Fenster heraus, das dabei selbst verschwinden muss. → Aufgabe 6.
|
||||
5. **Zwei Einstellungsfenster gleichzeitig.** Solange das JavaFX-Fenster noch existiert, können beide offen sein und gegeneinander schreiben. → Aufgabe 7.
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 1: `DataTable` für editierbare einspaltige Listen
|
||||
|
||||
Deckt Prüfschwerpunkt 3 ab.
|
||||
|
||||
**Dateien:**
|
||||
- Anlegen: `app-desktop/src/main/kotlin/kst4contest/view/compose/DataTable.kt`
|
||||
- Test: `app-desktop/src/test/kotlin/kst4contest/view/compose/EditableListStateTest.kt`
|
||||
|
||||
**Schnittstellen:**
|
||||
- Erzeugt: `EditableListState` mit `entries: List<String>`, `selectedIndex: Int?`, `addAtTop(String)`, `updateAt(Int, String)`, `removeAt(Int)`, `select(Int?)`, `commitTo(SimpleRoster<String>)`; und die Composable `DataTable(state, onCommit, modifier)`. Konsumiert von den Aufgaben 4 und 5.
|
||||
|
||||
Die drei Listen des Einstellungsfensters verhalten sich heute so: **Add** legt einen Platzhaltertext an **Position 0** ein, wählt ihn aus und öffnet die Bearbeitung; ein Eintrag, dessen Text vollständig geleert wird, **verschwindet**. Beides ist in `Kst4ContestApplication` beim Knopf „Add shortcut" und im `setOnEditCommit` nachzulesen und muss erhalten bleiben.
|
||||
|
||||
- [ ] **Schritt 1: Das heutige Verhalten ablesen**
|
||||
|
||||
Ausführen:
|
||||
```bash
|
||||
grep -n -A24 "private TableView<String> initShortcutTable" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
|
||||
grep -n -B2 -A12 "btn_Short_addLine.setOnAction" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
|
||||
```
|
||||
|
||||
Notieren: was `setOnEditCommit` mit einem leeren Text tut, wo ein neuer Eintrag landet, und was mit der Auswahl passiert. Der Test im nächsten Schritt schreibt **dieses** Verhalten fest, nicht ein besseres.
|
||||
|
||||
- [ ] **Schritt 2: Den fehlschlagenden Test schreiben**
|
||||
|
||||
`app-desktop/src/test/kotlin/kst4contest/view/compose/EditableListStateTest.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
import kst4contest.observe.SimpleRoster
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertSame
|
||||
|
||||
class EditableListStateTest {
|
||||
|
||||
@Test
|
||||
fun `a new entry lands at the top and becomes the selection`() {
|
||||
val state = EditableListState(listOf("alt"))
|
||||
|
||||
state.addAtTop("neu")
|
||||
|
||||
assertEquals(listOf("neu", "alt"), state.entries)
|
||||
assertEquals(0, state.selectedIndex,
|
||||
"the JavaFX table selected row 0 and opened it for editing")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an entry edited to blank disappears`() {
|
||||
val state = EditableListState(listOf("a", "b", "c"))
|
||||
|
||||
state.updateAt(1, " ")
|
||||
|
||||
assertEquals(listOf("a", "c"), state.entries,
|
||||
"clearing the text was how the operator removed an entry")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an edit keeps the other entries and their order`() {
|
||||
val state = EditableListState(listOf("a", "b", "c"))
|
||||
|
||||
state.updateAt(1, "B")
|
||||
|
||||
assertEquals(listOf("a", "B", "c"), state.entries)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removing the last entry clears the selection`() {
|
||||
val state = EditableListState(listOf("a"))
|
||||
|
||||
state.select(0)
|
||||
state.removeAt(0)
|
||||
|
||||
assertEquals(emptyList<String>(), state.entries)
|
||||
assertNull(state.selectedIndex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an index outside the list is ignored`() {
|
||||
val state = EditableListState(listOf("a"))
|
||||
|
||||
state.updateAt(5, "x")
|
||||
state.removeAt(-1)
|
||||
|
||||
assertEquals(listOf("a"), state.entries,
|
||||
"a stale index from a pending edit must not corrupt the list")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `committing fills the existing roster instead of replacing it`() {
|
||||
val roster = SimpleRoster<String>()
|
||||
roster.add("alt")
|
||||
val before = roster
|
||||
val state = EditableListState(listOf("neu"))
|
||||
|
||||
state.commitTo(roster)
|
||||
|
||||
assertEquals(listOf("neu"), roster.snapshot())
|
||||
assertSame(before, roster,
|
||||
"replacing the roster would orphan every listener registered on it")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `committing an unchanged list still yields the same content`() {
|
||||
val roster = SimpleRoster<String>()
|
||||
roster.addAll(listOf("a", "b"))
|
||||
val state = EditableListState(roster.snapshot())
|
||||
|
||||
state.commitTo(roster)
|
||||
|
||||
assertEquals(listOf("a", "b"), roster.snapshot())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: Test laufen lassen und Fehlschlag sehen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.EditableListStateTest'`
|
||||
Erwartet: FAIL — `Unresolved reference 'EditableListState'`.
|
||||
|
||||
- [ ] **Schritt 4: Den Zustand schreiben**
|
||||
|
||||
`app-desktop/src/main/kotlin/kst4contest/view/compose/DataTable.kt`, erster Teil:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
import kst4contest.observe.SimpleRoster
|
||||
|
||||
/**
|
||||
* Editing state of a single-column string list, free of Compose so the rules are
|
||||
* testable without a toolkit.
|
||||
*
|
||||
* Reproduces what the JavaFX tables in the settings window did: a new entry goes
|
||||
* to the top and becomes the selection, and an entry edited to blank disappears —
|
||||
* that was how the operator removed one.
|
||||
*/
|
||||
class EditableListState(initial: List<String>) {
|
||||
|
||||
private val items: MutableList<String> = initial.toMutableList()
|
||||
|
||||
val entries: List<String>
|
||||
get() = items.toList()
|
||||
|
||||
var selectedIndex: Int? = null
|
||||
private set
|
||||
|
||||
fun select(index: Int?) {
|
||||
selectedIndex = index?.takeIf { it in items.indices }
|
||||
}
|
||||
|
||||
fun addAtTop(entry: String) {
|
||||
items.add(0, entry)
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
/** An index outside the list is ignored: a pending edit can outlive its row. */
|
||||
fun updateAt(index: Int, text: String) {
|
||||
if (index !in items.indices) {
|
||||
return
|
||||
}
|
||||
if (text.isBlank()) {
|
||||
removeAt(index)
|
||||
return
|
||||
}
|
||||
items[index] = text
|
||||
}
|
||||
|
||||
fun removeAt(index: Int) {
|
||||
if (index !in items.indices) {
|
||||
return
|
||||
}
|
||||
items.removeAt(index)
|
||||
if (selectedIndex?.let { it >= items.size } == true) {
|
||||
selectedIndex = items.indices.lastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the given roster. Deliberately setAll and not a field swap: replacing
|
||||
* the roster would orphan every listener registered on it, which is the same
|
||||
* rule the ChatPreferences setters follow since Etappe 2.
|
||||
*/
|
||||
fun commitTo(roster: SimpleRoster<String>) {
|
||||
roster.setAll(items)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 5: Test laufen lassen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.EditableListStateTest'`
|
||||
Erwartet: PASS, 7 Testfälle.
|
||||
|
||||
- [ ] **Schritt 6: Die Composable schreiben**
|
||||
|
||||
An `DataTable.kt` anfügen. Zwei Punkte sind nicht verhandelbar:
|
||||
|
||||
- `LazyColumn` mit `key = { index, _ -> index }` ist **falsch** — bei einer Einfügung an Position 0 verschieben sich alle Schlüssel und die Auswahl springt. Der Schlüssel muss die **Identität des Eintrags** tragen. Weil Zeichenketten doppelt vorkommen können, bekommt jeder Eintrag beim Anlegen eine laufende Nummer, die `EditableListState` mitführt.
|
||||
- Der Fokus geht beim Anlegen eines Eintrags ausdrücklich in das neue Textfeld, über einen `FocusRequester`. Nicht hoffen, dass es von selbst passiert: in Etappe 2 waren zwei von drei Fehlern Fokusfehler.
|
||||
|
||||
Ergibt sich daraus, dass `EditableListState` Einträge nicht als `String`, sondern als `(id, text)` führen muss, ist der Test aus Schritt 2 entsprechend nachzuziehen — **vor** der Umsetzung, und als `Ruling:` ins Ledger.
|
||||
|
||||
- [ ] **Schritt 7: Gesamtlauf und Commit**
|
||||
|
||||
Ausführen: `./gradlew clean build`
|
||||
Erwartet: BUILD SUCCESSFUL, **272** Testfälle (265 + 7), 0 Fehler.
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Add an editable single-column table for Compose windows"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 2: Das Sicherheitsnetz — Bestandsaufnahme der Einstellungsfelder
|
||||
|
||||
Deckt die Prüfschwerpunkte 1 und 2 ab. **Ohne diese Aufgabe ist der Rest nicht abnehmbar.**
|
||||
|
||||
**Dateien:**
|
||||
- Anlegen: `app-desktop/src/test/resources/settings-fields.txt` (erzeugt, dann eingecheckt)
|
||||
- Anlegen: `app-desktop/src/test/kotlin/kst4contest/view/compose/SettingsFieldCoverageTest.kt`
|
||||
|
||||
**Schnittstellen:**
|
||||
- Erzeugt: die Prüfung, an der jede folgende Aufgabe gemessen wird.
|
||||
|
||||
Das JavaFX-Einstellungsfenster ruft **72 verschiedene** `ChatPreferences`-Setter.
|
||||
|
||||
> Korrigiert am 2026-09-27: Der Plan nannte zuvor 93. Das waren rohe Setter-**Aufrufe** im Bereich, inklusive JavaFX-Steuerelementen wie `setDisable` und `setPrefWidth`. Die belastbare Zahl entsteht durch Schnitt mit den Settern, die `ChatPreferences` wirklich deklariert: **72**. Ein beim Portieren vergessenes Feld fällt nicht beim Übersetzen auf, sondern wenn ein Operator im Contest merkt, dass seine Einstellung weg ist. Diese Aufgabe macht daraus eine Zahl, die der Build prüft.
|
||||
|
||||
Korrektur zum Spec: es sind **elf** Reiter, nicht zwölf. `Macros` ist auskommentiert (`Kst4ContestApplication:12346`). Aktiv sind Station, Log synch, TRX synch, Airscout, Notification, Shortcuts, Beacon, Messagehandling, Workedstn database, GUI und Profiles.
|
||||
|
||||
- [ ] **Schritt 1: Die Bestandsaufnahme erzeugen**
|
||||
|
||||
Mechanisch, nicht nach Urteil: die Setter-Aufrufe im Bereich, geschnitten mit den Settern, die `ChatPreferences` wirklich deklariert. Damit fallen JavaFX-Steuerelement-Setter ohne Einzelfallentscheidung heraus.
|
||||
|
||||
```bash
|
||||
awk 'NR>=9588 && NR<=12400' app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
|
||||
| grep -oE "\.set[A-Za-z_0-9]+\(" | sed 's/^\.//;s/(//' | sort -u > /tmp/setters-raw.txt
|
||||
grep -oE "public void (set[A-Za-z_0-9]+)\(" core/src/main/java/kst4contest/model/ChatPreferences.java \
|
||||
| sed 's/public void //;s/(//' | sort -u > /tmp/prefs-setters.txt
|
||||
comm -12 /tmp/setters-raw.txt /tmp/prefs-setters.txt > app-desktop/src/test/resources/settings-fields.txt
|
||||
wc -l < app-desktop/src/test/resources/settings-fields.txt
|
||||
```
|
||||
Erwartet: **72**.
|
||||
|
||||
Die Zeilennummern 9588 und 12400 vorher gegen den Ist-Stand prüfen:
|
||||
```bash
|
||||
grep -n "settingsStage = new Stage()" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
|
||||
grep -n 'Tab tbProfiles = new Tab' app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
|
||||
```
|
||||
|
||||
Dann die **ausgeschlossenen** durchsehen — `comm -23 /tmp/setters-raw.txt /tmp/prefs-setters.txt` — und bei jedem Namen prüfen, der nicht offensichtlich ein Steuerelement ist. Zwei Fälle sind belegt: `setLoginChatCategory` steht nur in einer auskommentierten Zeile, die echten Aufrufe heissen `…Main` und `…Second`; `setMYQRGFirstCat` fehlt zu Recht, die setzt die TRX-Synchronisation zur Laufzeit. Jede solche Prüfung als `Ruling:` ins Ledger — hier fällt ein echtes Feld unbemerkt heraus, wenn man es übergeht.
|
||||
|
||||
- [ ] **Schritt 2: Den Test schreiben, der die Abdeckung prüft**
|
||||
|
||||
`app-desktop/src/test/kotlin/kst4contest/view/compose/SettingsFieldCoverageTest.kt`:
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Guards against a setting quietly disappearing during the port.
|
||||
*
|
||||
* The JavaFX settings window called 93 distinct ChatPreferences setters. Every one
|
||||
* of them must still be called from the Compose settings sources, or an operator
|
||||
* loses a setting they could configure before — and nothing about that fails to
|
||||
* compile.
|
||||
*/
|
||||
class SettingsFieldCoverageTest {
|
||||
|
||||
@Test
|
||||
fun `every setting the JavaFX window could write is still written`() {
|
||||
val expected = File("src/test/resources/settings-fields.txt")
|
||||
.readLines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||
|
||||
val composeSources = File("src/main/kotlin/kst4contest/view/compose")
|
||||
.walkTopDown().filter { it.extension == "kt" }
|
||||
.joinToString("\n") { it.readText() }
|
||||
|
||||
val missing = expected.filterNot { composeSources.contains("$it(") }
|
||||
|
||||
assertTrue(missing.isEmpty(),
|
||||
"settings not written by any Compose source (${missing.size} of ${expected.size}):\n" +
|
||||
missing.joinToString("\n") { " $it" })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: Test laufen lassen und Fehlschlag sehen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.SettingsFieldCoverageTest'`
|
||||
Erwartet: FAIL, mit `settings not written by any Compose source (72 of 72)`. Das ist der Ausgangspunkt; jede folgende Aufgabe verkleinert die Liste.
|
||||
|
||||
> Der Test prüft Textvorkommen, nicht Laufzeitverhalten. Er kann nicht erkennen, ob ein Setter mit dem richtigen Wert gerufen wird — nur, dass er überhaupt gerufen wird. Das ist wenig, aber es ist die einzige Prüfung, die 93 Felder mechanisch abdeckt, und sie fällt sofort auf, wenn eines fehlt.
|
||||
|
||||
- [ ] **Schritt 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Pin down which settings the port must keep writable"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 3: Das Fenster und der erste Reiter
|
||||
|
||||
**Dateien:**
|
||||
- Anlegen: `app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsWindow.kt`
|
||||
- Anlegen: `app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/StationTab.kt`
|
||||
- Test: `app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/StationTabStateTest.kt`
|
||||
- Ändern: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
|
||||
|
||||
**Schnittstellen:**
|
||||
- Konsumiert: `Kst4ContestTheme` und den Weg aus Etappe 3a (`application(exitProcessOnExit = false)` in eigenem Thread).
|
||||
- Erzeugt: `SettingsWindow.show(preferences, autosave, onProfileSwitch)` und das Muster `<Name>TabState`, dem die Aufgaben 4 bis 6 folgen.
|
||||
|
||||
Anders als der Profil-Dialog ist dieses Fenster **nicht blockierend**: der Operator arbeitet weiter, während es offen ist. Es darf daher nicht mit einem Latch auf das Schließen warten.
|
||||
|
||||
- [ ] **Schritt 1: Den heutigen Aufbau ablesen**
|
||||
|
||||
Ausführen: `sed -n '9588,9640p' app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
|
||||
|
||||
Notieren: Modalität, Größe, Titel, ob das Fenster wiederverwendet oder neu gebaut wird, und wie **Save Settings** angebunden ist. Laut `PROJECT_CONTEXT` ist **Save Settings** der vollständige Schreiber und enthält das aktuelle Layout — das muss so bleiben.
|
||||
|
||||
- [ ] **Schritt 2: Den Zustand des Station-Reiters mit Test**
|
||||
|
||||
Zuerst der Test. Er prüft, dass der Zustand aus `ChatPreferences` liest und **nur beim Speichern** zurückschreibt — nicht bei jedem Tastendruck, sonst landen unbestätigte Werte in der Datei. `PROJECT_CONTEXT` verlangt genau das: *„Selective layout writes … must not persist unconfirmed functional settings from the current UI."*
|
||||
|
||||
```kotlin
|
||||
package kst4contest.view.compose.tabs
|
||||
|
||||
import kst4contest.model.ChatPreferences
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
|
||||
class StationTabStateTest {
|
||||
|
||||
@Test
|
||||
fun `the state starts from the stored preferences`() {
|
||||
val prefs = ChatPreferences()
|
||||
prefs.stn_loginCallSign = "DN9APW"
|
||||
|
||||
val state = StationTabState(prefs)
|
||||
|
||||
assertEquals("DN9APW", state.callSign)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `editing does not touch the preferences until apply`() {
|
||||
val prefs = ChatPreferences()
|
||||
prefs.stn_loginCallSign = "DN9APW"
|
||||
val state = StationTabState(prefs)
|
||||
|
||||
state.callSign = "DO5AMF"
|
||||
|
||||
assertNotEquals("DO5AMF", prefs.stn_loginCallSign,
|
||||
"an unconfirmed edit must not reach preferences.xml")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `apply writes every field of this tab`() {
|
||||
val prefs = ChatPreferences()
|
||||
val state = StationTabState(prefs)
|
||||
|
||||
state.callSign = "DO5AMF"
|
||||
state.apply()
|
||||
|
||||
assertEquals("DO5AMF", prefs.stn_loginCallSign)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Die Feldnamen gegen `ChatPreferences` prüfen, bevor der Test geschrieben wird — `grep -n "stn_login" core/src/main/java/kst4contest/model/ChatPreferences.java`.
|
||||
|
||||
- [ ] **Schritt 3: Fehlschlag sehen, dann umsetzen**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.tabs.StationTabStateTest'`
|
||||
Erwartet: FAIL — `StationTabState` existiert nicht.
|
||||
|
||||
Dann `StationTabState` und `StationTab` schreiben. **Alle** Felder des Reiters, nicht nur das Rufzeichen; die Liste aus `settings-fields.txt` und der Blick in `vbxStation` sagen, welche dazugehören.
|
||||
|
||||
- [ ] **Schritt 4: Das Fenster schreiben und anbinden**
|
||||
|
||||
`SettingsWindow` mit Reiterleiste, zunächst nur dem Station-Reiter, plus **Save Settings** und **Cancel**. Anbinden an denselben Menüpunkt, der heute `settingsStage` öffnet — das JavaFX-Fenster bleibt vorerst erreichbar, damit die noch nicht portierten Reiter benutzbar sind.
|
||||
|
||||
> Prüfschwerpunkt 5: solange beide Fenster existieren, können sie gegeneinander schreiben. Der Menüpunkt öffnet deshalb **entweder** das eine **oder** das andere, nie beide. Welches, entscheidet ein Schalter im Quelltext, der in Aufgabe 7 verschwindet.
|
||||
|
||||
- [ ] **Schritt 5: Prüfen**
|
||||
|
||||
Ausführen: `./gradlew build && ./gradlew :app-desktop:run`
|
||||
|
||||
Erwartet: Das Compose-Einstellungsfenster öffnet, zeigt den Station-Reiter mit den gespeicherten Werten, **Save Settings** schreibt sie, und nach einem Neustart sind sie noch da. Das Hauptfenster bleibt währenddessen bedienbar — das Fenster ist nicht blockierend.
|
||||
|
||||
Ausführen: `grep -c "configVersion>7" ~/.praktiKST/preferences.xml`
|
||||
Erwartet: `1`.
|
||||
|
||||
- [ ] **Schritt 6: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Open the settings window in Compose with the station tab"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufgaben 4 bis 6: Die übrigen zehn Reiter
|
||||
|
||||
Drei Aufgaben, gruppiert nach Verwandtschaft, nicht nach Reihenfolge im Quelltext. Jede folgt demselben Muster wie Aufgabe 3 und endet damit, dass `SettingsFieldCoverageTest` weniger fehlende Felder meldet als vorher.
|
||||
|
||||
> **Warum hier kein Quelltext steht, anders als in den Aufgaben 1 bis 3.** Zehn Reiter mit zusammen 93 Feldern auszuschreiben würde diesen Plan verdoppeln und dabei nur wiederholen, was schon zweimal festgeschrieben ist: in `settings-fields.txt` steht, **welche** Felder es sind, und Aufgabe 3 zeigt an einem vollständigen Beispiel, **wie** ein Reiter gebaut wird. Was dem Umsetzer fehlen könnte, ist keine Vorlage, sondern die Zuordnung Feld zu Reiter — und die steht im Quelltext, nicht in einem Plan, der sie abschreibt.
|
||||
>
|
||||
> Das ist eine Abweichung von der Regel, dass jeder Schritt seinen Inhalt mitbringt. Sie ist bewusst: bei mechanischer Wiederholung ist eine geprüfte Vorlage plus eine mechanische Abnahme mehr wert als zehn abgeschriebene Formulare, in denen sich Fehler verstecken.
|
||||
|
||||
**Für jeden Reiter, in dieser Reihenfolge:**
|
||||
|
||||
1. Die Felder des Reiters aus `settings-fields.txt` und dem zugehörigen `vbx…`-Block in `Kst4ContestApplication` heraussuchen.
|
||||
2. Test für `<Name>TabState` schreiben: liest aus den Preferences und schreibt **sofort** zurück, für **alle** Felder des Reiters. Kein `apply()`.
|
||||
3. Fehlschlag sehen.
|
||||
4. `<Name>TabState` und `<Name>Tab` schreiben.
|
||||
5. Test grün.
|
||||
6. Reiter in `SettingsWindow` eintragen.
|
||||
7. `SettingsFieldCoverageTest` laufen lassen und die neue Zahl fehlender Felder ins Ledger.
|
||||
8. **Den JavaFX-Block des Reiters auf Schreibstellen ausserhalb der Einstellungen durchsehen.** Das erste Netz sieht sie nicht. Gefundene Stellen als Rückruf in die Zustandsklasse und in `settings-foreign-writes.txt` nachtragen, falls sie dort noch fehlen.
|
||||
|
||||
### Aufgabe 4: Log synch, TRX synch, Airscout
|
||||
|
||||
**Dateien:** `tabs/LogSynchTab.kt`, `tabs/TrxSynchTab.kt`, `tabs/AirscoutTab.kt` samt Zustandsklassen und Tests.
|
||||
|
||||
Diese drei hängen an externen Schnittstellen. **Ports und Transporte werden nicht angetastet** — weder Vorgabewerte noch Wertebereiche. `AGENTS.md` ist hier ausdrücklich: Framing, Ports und Transportannahmen sind Protokollverhalten.
|
||||
|
||||
Achtung bei TRX synch: laut `PROJECT_CONTEXT` verlangen automatische QRG-Aktualisierungen **beide** Dinge — eine aktivierte Quelle **und** gültige eingehende Daten. Das Einschalten allein liefert keine QRG. Der Reiter darf daher nicht suggerieren, dass eine aktivierte Quelle schon eine Frequenz bedeutet.
|
||||
|
||||
### Aufgabe 5: Notification, Shortcuts, Messagehandling, Beacon
|
||||
|
||||
**Dateien:** `tabs/NotificationTab.kt`, `tabs/ShortcutsTab.kt`, `tabs/MessageHandlingTab.kt`, `tabs/BeaconTab.kt` samt Zustandsklassen und Tests.
|
||||
|
||||
**Hier kommt die `DataTable` aus Aufgabe 1 zum Einsatz**, dreimal:
|
||||
|
||||
| Liste | Roster in `ChatPreferences` |
|
||||
|---|---|
|
||||
| Überwachte Rufzeichen (Notification) | `lstNotify_QSOSniffer_sniffedCallSignList` (über `ChatController`) |
|
||||
| Textkürzel (Shortcuts) | `lst_txtShortCutBtnList` |
|
||||
| Textbausteine (Messagehandling) | `lst_txtSnipList` |
|
||||
|
||||
Beim Speichern wird `EditableListState.commitTo(roster)` gerufen — es füllt den vorhandenen Roster, statt ihn zu ersetzen. Ein Feldtausch würde angemeldete Zuhörer abwerfen; genau deshalb hat Etappe 2 die `ChatPreferences`-Setter auf `setAll` umgestellt.
|
||||
|
||||
Der Beacon-Reiter enthält die Vorgabetexte mit Platzhaltern wie `MYCALL` und `MYQRG`. Diese Zeichenketten sind **Literale und bleiben unverändert** — `MessageVariableResolver` löst sie auf, und eine geänderte Schreibweise bricht das still.
|
||||
|
||||
### Aufgabe 6: Workedstn database, GUI, Profiles
|
||||
|
||||
**Dateien:** `tabs/WorkedDatabaseTab.kt`, `tabs/GuiOptionsTab.kt`, `tabs/ProfilesTab.kt` samt Zustandsklassen und Tests.
|
||||
|
||||
Deckt Prüfschwerpunkt 4 ab.
|
||||
|
||||
**Workedstn database** enthält den Zurücksetzen-Knopf. Die Worked-Semantik ist unverändert: normalisierter Basisruf als Schlüssel, Worked-Zustand über Suffixvarianten geteilt, drei Tage Ablauf, manuelles Zurücksetzen. Ein Zurücksetzen fragt vorher nach.
|
||||
|
||||
**GUI** enthält unter anderem `GUIstationMapClusteringEnabled`. Laut `PROJECT_CONTEXT` ist das eine Layout-Einstellung unterhalb `guiOptions` mit Vorgabewert `true`, selektiv autogespeichert; fehlende oder fehlerhafte Werte behalten `true`. Diese drei Eigenschaften bleiben.
|
||||
|
||||
**Profiles** ist der schwierigste Reiter dieses Plans. Er ersetzt `OperatorProfileSettingsPane` (488 Zeilen) und löst einen **Laufzeitwechsel** aus: `ApplicationRuntimeLauncher` reißt die Laufzeit ab und baut eine neue `Kst4ContestApplication`. Das Compose-Einstellungsfenster muss dabei selbst verschwinden — es gehört zur alten Laufzeit.
|
||||
|
||||
Zu klären und als `Ruling:` festzuhalten: schließt das Fenster sich vor dem Wechsel selbst, oder räumt `shutdownRuntime()` es ab? Die zweite Variante braucht eine Rückmeldung von Compose nach Java. Die erste ist einfacher und vermutlich richtig.
|
||||
|
||||
> In Etappe 3a wurde bestätigt, dass ein Profilwechsel mit einem Compose-Fenster im Spiel funktioniert. Damals war das Fenster der Profil-Dialog, der ohnehin schließt. Hier bleibt ein Fenster offen — das ist der neue Teil.
|
||||
|
||||
Bekannter Nebenbefund, **vorbestehend**, der beim Prüfen im Protokoll erscheinen wird: Beim Profilwechsel stirbt der UCX-Log-UDP-Zuhörer der alten Laufzeit mit `NullPointerException`, `BindException` und der Meldung `Program Restart needed`, weil `ReadUDPbyUCXMessageThread` eine `while (true)`-Schleife ohne Abbruchbedingung hat. Für die neue Laufzeit kommt ein frischer Zuhörer hoch, die Funktion erholt sich. **Das ist nicht von dieser Etappe** und wird hier nicht behoben.
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe 7: Das JavaFX-Einstellungsfenster entfernen
|
||||
|
||||
Deckt Prüfschwerpunkt 5 ab.
|
||||
|
||||
**Dateien:**
|
||||
- Ändern: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java` (`settingsStage` und der zugehörige Aufbau, etwa `:9588`–`:12400`)
|
||||
- Löschen: `app-desktop/src/main/java/kst4contest/view/OperatorProfileSettingsPane.java` (488 Zeilen)
|
||||
|
||||
- [ ] **Schritt 1: Erst prüfen, dass nichts fehlt**
|
||||
|
||||
Ausführen: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.SettingsFieldCoverageTest'`
|
||||
Erwartet: PASS. **Solange dieser Test rot ist, wird nichts gelöscht.**
|
||||
|
||||
- [ ] **Schritt 2: Den Schalter und das alte Fenster entfernen**
|
||||
|
||||
Den Schalter aus Aufgabe 3 Schritt 4 löschen, den `settingsStage`-Aufbau entfernen, `OperatorProfileSettingsPane` löschen.
|
||||
|
||||
Ausführen: `grep -rn "settingsStage\|OperatorProfileSettingsPane" app-desktop/src`
|
||||
Erwartet: keine Ausgabe.
|
||||
|
||||
- [ ] **Schritt 3: Zählstand**
|
||||
|
||||
Ausführen: `wc -l app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
|
||||
|
||||
Die Datei begann Etappe 2 mit 13.564 Zeilen. Der neue Wert gehört ins Ledger: er ist das erste harte Maß dafür, dass die God-Klasse schrumpft.
|
||||
|
||||
- [ ] **Schritt 4: Die Abnahme von Hand**
|
||||
|
||||
Ausführen: `./gradlew clean build && ./gradlew :app-desktop:packageImage` — beziehungsweise `createDistributable`, je nachdem was Etappe 3a hinterlassen hat — und das Paket starten.
|
||||
|
||||
Alle elf Reiter durchgehen und in **jedem** mindestens einen Wert ändern. Dann **Save Settings**, Programm beenden, neu starten, und prüfen dass alle Änderungen noch da sind.
|
||||
|
||||
Ausführen danach:
|
||||
```bash
|
||||
grep -c "configVersion>7" ~/.praktiKST/preferences.xml
|
||||
python3 -c "import xml.etree.ElementTree as E; E.parse('$HOME/.praktiKST/preferences.xml'); print('XML gültig')"
|
||||
```
|
||||
Erwartet: `1` und `XML gültig`.
|
||||
|
||||
**Vorher eine Sicherung anlegen**: `cp ~/.praktiKST/preferences.xml ~/.praktiKST/preferences.xml.vor-etappe3b`. Diese Aufgabe schreibt in die echte Konfiguration des Operators.
|
||||
|
||||
- [ ] **Schritt 5: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Remove the JavaFX settings window"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abnahme der Etappe 3b
|
||||
|
||||
- [ ] `./gradlew clean build` läuft durch, 0 Fehler.
|
||||
- [ ] `SettingsFieldCoverageTest` ist grün: alle 72 Einstellungen werden von Compose-Quellen geschrieben.
|
||||
- [ ] `SettingsForeignWriteCoverageTest` ist grün: alle fünf Schreibstellen ausserhalb der Einstellungen werden ausgeführt.
|
||||
- [ ] `grep -rn "settingsStage\|OperatorProfileSettingsPane" app-desktop/src` liefert keine Ausgabe.
|
||||
- [ ] Alle elf Reiter sind bedienbar, und in jedem übersteht eine Änderung **Save Settings** plus Neustart.
|
||||
- [ ] `preferences.xml` bleibt `configVersion 7`, ist gültiges XML, und unbekannte Knoten sind erhalten.
|
||||
- [ ] Die drei editierbaren Listen können Einträge anlegen, ändern und durch Leeren entfernen — wie vorher.
|
||||
- [ ] Der Profilwechsel über den Reiter **Profiles** funktioniert, und das Einstellungsfenster verschwindet dabei.
|
||||
- [ ] `core` ist unberührt: `grep -rn "javafx" core/src` liefert nichts.
|
||||
- [ ] Die neue Zeilenzahl von `Kst4ContestApplication.java` steht im Ledger.
|
||||
|
||||
## Was danach kommt
|
||||
|
||||
Etappe 4 nach Spec: Update-Fenster und Monitor-Fenster. Das Monitor-Fenster belastet die `DataTable` erstmals mit **Live-Daten** und mit mehreren Spalten — dort entsteht, was dieser Plan bewusst nicht gebaut hat: Spaltenbreiten mit stabilen IDs, Sortierung und Zeilen-Styling. Die Erfahrung aus diesem Plan gehört dann in dessen Entwurf.
|
||||
|
||||
## Befunde aus Gruppe B (Aufgaben 4–6)
|
||||
|
||||
Gruppe B hat ihre vier Tests geschrieben, bevor die Umsetzungen existierten. Ich
|
||||
habe **jede** darin behauptete Verhaltensweise gegen den JavaFX-Quelltext geprüft,
|
||||
bevor ich sie umgesetzt habe. Ergebnis: alle Behauptungen waren richtig. Belege:
|
||||
|
||||
| Behauptung | Belegstelle in `Kst4ContestApplication.java` |
|
||||
|---|---|
|
||||
| Platzhaltertext neuer Listeneinträge | `:11743` und `:11797`, in beiden Listen wörtlich gleich |
|
||||
| Leerer Text entfernt den Eintrag | `ShortCol.setOnEditCommit`, `newValue.isBlank()` → `remove` |
|
||||
| „Move selected up/down" existiert in beiden Listen | `:11753`, `:11760`, `:11806`, `:11813` |
|
||||
| Eine Ankreuzfläche schreibt beide Kategorien | `:12004` und `:12005` |
|
||||
| Ein Textfeld schreibt beide Kategorietexte | `:12039` und `:12040` |
|
||||
| QRG-Antwort ist davon unabhängig | `:12026` |
|
||||
| Port 1–65535, Vorwert bei Ablehnung | `:11243`–`:11282` |
|
||||
| Neustart nur bei geändertem Port | `:11262`, `port != previousPort` |
|
||||
| Spotter-Rufzeichen wird großgeschrieben und geprüft | `:11390`–`:11420` |
|
||||
| Unbekanntes Frequenzpräfix wird auf 144 repariert | `:11349`–`:11356` |
|
||||
| Testspot: `DO5AMF`, `300`, `DXC test: You donated $100!` | `:11469`–`:11473` |
|
||||
| Überwachte Rufzeichen: Basisrufzeichen, **angehängt**, Doppelprüfung ohne Groß-/Kleinschreibung | `:11635`–`:11700` |
|
||||
| Vorlage prüfen, Vorwert bleibt bei Ablehnung | `applyBeaconTextSetting`, `:5271` |
|
||||
| Intervall in beide Kategorien, Zeitgeber nur bei Erfolg | `applySharedBeaconInterval`, `:5306` |
|
||||
|
||||
Zwei Stellen gehen bewusst über den JavaFX-Stand hinaus, beide dokumentiert im
|
||||
Quelltext:
|
||||
|
||||
1. **`beaconIntervalMinutes` klemmt beim Lesen auf den Mindestwert.** JavaFX wandte
|
||||
`Math.max` nur im Fehlerzweig an; `ChatController:3425` klemmt aber ohnehin beim
|
||||
Senden. Ein gespeicherter Nullwert wurde also als gültige Einstellung angezeigt,
|
||||
obwohl gar nicht danach gesendet wurde.
|
||||
2. **Das Auffrischen nach dem Hinzufügen eines Eintrags.** JavaFX frischte nur beim
|
||||
Bearbeitungsabschluss und beim Verschieben auf, nicht beim Hinzufügen — dort folgte
|
||||
das Bearbeiten aber unmittelbar. Beim Übergeben aufzufrischen ist derselbe Endstand.
|
||||
|
||||
### Was mein eigener Aufgabe-1-Bericht übersehen hatte
|
||||
|
||||
Ich hatte die Verschiebeknöpfe der Listen nicht erfasst. Gruppe B hat sie in
|
||||
`EditableListState.moveSelected` vorgesehen; die Knöpfe existieren in beiden Listen
|
||||
wirklich. Ohne die Prüfung hätte ich Bs Arbeit für erfunden gehalten.
|
||||
|
||||
### Grenzen der Netze, erneut bestätigt
|
||||
|
||||
Netz 2 sah die drei Testspot-Schreibvorgänge zunächst nicht, weil ich sie als
|
||||
Kotlin-Eigenschaftszuweisung geschrieben hatte (`spot.qra = …`) und das Netz nach
|
||||
`setQra(` sucht. Das Netz prüft **Text, nicht Verhalten**. Behoben, indem die
|
||||
Java-Setzer namentlich aufgerufen werden — was hier ohnehin die treuere Entsprechung
|
||||
ist. Wer künftig eine Zuweisung bevorzugt, muss das Netz mitändern, sonst wird es
|
||||
still nutzlos.
|
||||
|
||||
### Stand
|
||||
|
||||
- Beide Netze grün: **72 von 72** Feldern erfasst, **5 von 5** Fremdschreibvorgängen.
|
||||
- **362 Testfälle, 0 Fehler** (vorher 322 mit zwei absichtlich roten).
|
||||
- Verdrahtung in `SettingsTabs.kt`, zehn Reiter in der Reihenfolge der JavaFX-`TabPane`.
|
||||
|
||||
### Offen
|
||||
|
||||
- `ProfilesTab` als Ersatz für `OperatorProfileSettingsPane` (488 Zeilen).
|
||||
- Aufgabe 7: `settingsStage` und `OperatorProfileSettingsPane` löschen, den Aufruf
|
||||
von `buildSettingsTabs`/`SettingsWindow.show` in `Kst4ContestApplication` setzen und
|
||||
`SettingsHost` dort umsetzen. Neue Zeilenzahl festhalten (Anfang Etappe 2: 13 564).
|
||||
- Abnahme von Hand: alle Reiter, je eine Änderung, „Save settings", Neustart,
|
||||
`configVersion 7` unversehrt. Vorher `~/.praktiKST/preferences.xml` sichern.
|
||||
|
||||
## Aufgabe 7: was der Plan nicht wusste
|
||||
|
||||
Der Plan sagte „`settingsStage` und `OperatorProfileSettingsPane` löschen". Beim
|
||||
Lesen des Blocks kam heraus, dass das Einstellungsfenster **mehr war als
|
||||
Einstellungen**: seine Knopfleiste trug
|
||||
|
||||
| Knopf | Wirkung |
|
||||
|---|---|
|
||||
| `btnOptionspnlConnect` | `chatcontroller.execute()` — die **einzige** Stelle, an der der Chat startet |
|
||||
| `btnOptionspnlDisconnect` | `closeWindowEvent(null)`, Chat schließen |
|
||||
| `btnOptionspnlDisconnectOnly` | `disconnect(DISCSTRING_DISCONNECTONLY)` |
|
||||
| `btn_preferences_saveAsDefault` | Einstellungen sichern |
|
||||
| `btnOptionsPnlApply` | Fenster schließen |
|
||||
|
||||
Deshalb stand `settingsStage.show()` im Startpfad und nicht an einem Knopf: **das
|
||||
Einstellungsfenster war das Anmeldefenster.** Ein Löschen ohne Portieren hätte den
|
||||
Anmeldeweg entfernt. `ConnectionBar.kt` bildet die Leiste nach, in derselben
|
||||
Reihenfolge.
|
||||
|
||||
### Weitere Lücken, die dabei auffielen
|
||||
|
||||
1. **Die Kategorie-Auswahl fehlte im Compose-Station-Reiter.** `StationTabState`
|
||||
hatte `selectMainCategory`/`selectSecondCategory`, der Reiter zeigte sie nie.
|
||||
Ohne sie lässt sich der Chatraum nicht wählen — die Anwendung wäre unbenutzbar
|
||||
gewesen.
|
||||
2. **Die Sperre der Anmeldefelder fehlte.** JavaFX sperrte Rufzeichen, Kennwort,
|
||||
Name, Locator und beide Kategorien, sobald die Sitzung angemeldet war: eine
|
||||
Änderung erreicht die laufende Verbindung nicht.
|
||||
3. **Die Tabelle der gearbeiteten Stationen fehlte.** Ich hielt sie zunächst für
|
||||
tote Oberfläche, weil `tblVw_worked.setItems()` auskommentiert ist — falsch:
|
||||
`initWkdStnTable` setzt die Einträge selbst (`:5585`,
|
||||
`getLst_DBBasedWkdCallSignList()`). Jetzt nachgebaut, eine Zeile je Rufzeichen,
|
||||
eine Spalte je Band.
|
||||
4. **Reihenfolge und Titel der Reiter waren bei mir falsch.** Ich hatte sortiert
|
||||
und umbenannt; der JavaFX-Quelltext warnt ausdrücklich davor
|
||||
(„operators navigate these tabs by muscle memory"). Jetzt zeichengleich:
|
||||
Station, Log synch, TRX synch, Airscout, Notification, Shortcuts, Beacon,
|
||||
Messagehandling, Workedstn database, GUI, Profiles.
|
||||
|
||||
### Bewusste Abweichungen
|
||||
|
||||
- **„hide options" heißt jetzt „show options" und schaltet nicht um.** Ein
|
||||
Compose-Fenster kann auch am eigenen Rahmen geschlossen werden; eine Beschriftung,
|
||||
die den Zustand hier mitführt, würde veralten, ohne es zu erfahren.
|
||||
- **Die Fenstergröße wird gelesen, aber nicht zurückgeschrieben.** JavaFX hatte
|
||||
Listener auf `widthProperty`/`heightProperty`, die `GUIsettingsStageSceneSizeHW`
|
||||
pflegten. Offen.
|
||||
- **Grundschriftgröße 12sp festgelegt.** Es gibt keine Einstellung dafür; JavaFX
|
||||
nahm die Vorgabe der Plattform. Gleicher Wert wie im Profil-Wähler.
|
||||
- **`refreshWorkedStationsView` ist absichtlich leer.** Die Compose-Tabelle liest
|
||||
den Roster selbst; die JavaFX-Tabelle brauchte den Aufruf nur, weil ein
|
||||
`TableView` Änderungen in seinen Objekten nicht bemerkt.
|
||||
|
||||
### Nebenbefund, nicht behoben
|
||||
|
||||
`finalTblVwWorked` wurde von der **leeren** `new TableView<>()` zugewiesen, bevor
|
||||
`tblVw_worked = initWkdStnTable()` neu zuwies. Die drei `.refresh()`-Aufrufe trafen
|
||||
also ein weggeworfenes Objekt. Sichtbar wurde das nie, weil der Roster-Spiegel die
|
||||
angezeigte Tabelle ohnehin nachzieht. Mit dem Block gelöscht.
|
||||
|
||||
### Ergebnis
|
||||
|
||||
- `Kst4ContestApplication.java`: **13 755 → 10 881 Zeilen** (−2 874).
|
||||
Zum Vergleich: Anfang Etappe 2 waren es 13 564.
|
||||
- `OperatorProfileSettingsPane.java` (488 Zeilen) gelöscht.
|
||||
- **373 Testfälle, 0 Fehler.** Beide Netze grün.
|
||||
- Rauchtest: Anwendung startet, Compose läuft, keine Ausnahme. Weiter als bis zum
|
||||
Profil-Wähler kam der Test ohne Klick nicht.
|
||||
|
||||
### Abnahme von Hand, offen
|
||||
|
||||
1. Profil wählen → Einstellungsfenster muss erscheinen.
|
||||
2. **Anmelden**: Kategorie wählen, Verbinden. Der Knopf muss die Kategorie(n) im
|
||||
Text nennen. Danach müssen die Anmeldefelder gesperrt sein.
|
||||
3. Trennen und „Disconnect & close Chat".
|
||||
4. Je eine Änderung in allen elf Reitern, „Save settings", Neustart,
|
||||
`configVersion 7` unversehrt.
|
||||
5. Profil-Reiter: anlegen, umbenennen, verdoppeln, Betriebsart der gearbeiteten
|
||||
Stationen ändern, wechseln. Löschen zuletzt und nur an einem Wegwerfprofil.
|
||||
6. Menü „Options" und „Windows → show options" müssen das Fenster öffnen.
|
||||
|
||||
## Abnahme von Hand — durchgeführt am 2026-09-27
|
||||
|
||||
Abgenommen von Philipp (DN9APW) am laufenden Programm. Die Belege stammen aus einem
|
||||
Vergleich der `preferences.xml` gegen eine Sicherung, nicht aus Augenschein allein.
|
||||
|
||||
### Persistenz
|
||||
|
||||
- **`configVersion` bleibt 7.** 138 Werte vor der Abnahme, 138 danach, keiner neu,
|
||||
keiner entfallen. Das war das Hauptrisiko beim Schreiben.
|
||||
- `writePreferencesToXmlFile()` hat im ganzen Programm **genau einen** Aufrufer,
|
||||
`savePreferencesFromSettings()` in `Kst4ContestApplication`. Die geänderten Werte
|
||||
stehen also in der Datei, weil „Save settings" funktioniert — nicht, weil irgendein
|
||||
Automatismus nebenher geschrieben hätte. Die Layoutwerte sind davon getrennt:
|
||||
`LayoutAutosave` schreibt sie über `writeLayoutPreferencesToXmlFile()`.
|
||||
|
||||
### Alle neun Reiter mit Feldern sind belegt
|
||||
|
||||
| Reiter | Beleg in der XML |
|
||||
|---|---|
|
||||
| Station | `stn_antennaBeamWidthDeg` 50 → 20 |
|
||||
| Log synch | `logsynch_ucxUDPWkdCallListenerEnabled`, `logsynch_wintestNetworkListenerEnabled` |
|
||||
| TRX synch | `logsynch_wintestQrgSyncEnabled`, `logsynch_wintestUsePassQrg` |
|
||||
| Airscout | `asQry_airScoutClientName` KST → KST4CONTEST |
|
||||
| Notification | `notify_SimpleAudioNotificationsEnabled` true → false |
|
||||
| Beacon | Intervall 5 → 7, beide Kategorien freigeschaltet |
|
||||
| Messagehandling | `autoAnswerEnabled` und Antworttext, beide Kategorien |
|
||||
| GUI | `guiOptions_darkModeActiveByDefault` true → false |
|
||||
| Shortcuts | Listeneintrag `MYQRG` → `AAAA-TEST` |
|
||||
|
||||
Die zwei Reiter ohne Felder — **Workedstn database** und **Profiles** — wurden über
|
||||
ihre Aktionen abgenommen.
|
||||
|
||||
### Was die Abnahme darüber hinaus bewiesen hat
|
||||
|
||||
**Die „ein Bedienelement schreibt beide Kategorien"-Umsetzungen stimmen.** Beacon-
|
||||
Intervall, Beacon-Freigabe, automatische Antwort und Antworttext haben jeweils beide
|
||||
Kategoriefelder geschrieben. Das war gegen den JavaFX-Quelltext geprüft, aber bis zur
|
||||
Abnahme nie bis in die Datei verfolgt.
|
||||
|
||||
### Zwei Zuordnungsfehler bei der Auswertung
|
||||
|
||||
Beide sind es wert, notiert zu werden, weil sie dieselbe Ursache haben: **von einem
|
||||
Namen auf die Zugehörigkeit zu schließen.**
|
||||
|
||||
1. `logsynch_wintestUsePassQrg` wurde nach dem Präfix dem Log-synch-Reiter
|
||||
zugeschlagen. Tatsächlich gehört es zu `TrxSynchTabState` — der TRX-synch-Reiter
|
||||
zeigt vier Win-Test-Felder, die in der XML unter `logsynch_` liegen.
|
||||
2. Ein Skript, das Java-Setzernamen gegen XML-Elementnamen paarte, meldete vier
|
||||
Reiter als unbelegt. Die Namen unterscheiden sich:
|
||||
`setNotify_playSimpleSounds` schreibt `notify_SimpleAudioNotificationsEnabled`,
|
||||
`setGUI_darkModeActiveByDefault` schreibt `guiOptions_darkModeActiveByDefault`.
|
||||
|
||||
Wer die Abdeckung künftig auswertet, muss die Zugehörigkeit aus dem Quelltext der
|
||||
`*TabState`-Klassen nehmen, nicht aus dem Feldnamen.
|
||||
|
||||
### Nebenbefund, nicht behoben
|
||||
|
||||
`ChatPreferences` schreibt einige Beacon-Werte doppelt: `bcn_beaconIntervalInMinutesMainCat`
|
||||
und `beaconCQIntervalMinutes` tragen denselben Wert, ebenso die Freigabe-Flags.
|
||||
Vorbestehend. Relevant für jeden, der die XML von Hand anfasst.
|
||||
@@ -1,248 +0,0 @@
|
||||
# Umsetzungsplan Etappe 4 — Update-Fenster und Monitor-Fenster
|
||||
|
||||
Spezifikation: `docs/superpowers/specs/2026-09-25-compose-migration-design.md`, Etappe 4.
|
||||
|
||||
## Was diese Etappe wirklich ist
|
||||
|
||||
Die Spezifikation nennt zwei Fenster. Beim Lesen des Quelltextes zeigt sich, dass das
|
||||
untertreibt:
|
||||
|
||||
| Fenster | Stelle | braucht |
|
||||
|---|---|---|
|
||||
| Update | `:9458` | ein Textgitter, einen Verweis, **einen Baum** mit Änderungsprotokoll und bekannten Fehlern, Größenspeicherung |
|
||||
| Monitor | `:9396` | `initDXClusterTable` mit **8 Spalten** über `ClusterMessage`, `initChatToOtherMSGTable` mit **10 Spalten** über `ChatMessage` |
|
||||
|
||||
`DataTable` aus Etappe 3b kann **eine** Spalte Text — `ListEntry(key, text)`. Die
|
||||
mehrspaltige Tabelle existiert nicht. Das Monitor-Fenster ist also nicht der Abnehmer
|
||||
einer fertigen Komponente, sondern ihr Anlass.
|
||||
|
||||
**Etappe 5 braucht dieselbe Komponente** für die Stationsliste mit Filterleiste und
|
||||
Prioritätenspalten. Was hier entsteht, trägt also drei Tabellen, nicht zwei. Der
|
||||
Zuschnitt wird danach entschieden, nicht nach dem, was für diese zwei Fenster gerade
|
||||
reicht — aber auch nicht darüber hinaus: die Stationsliste wird gelesen, bevor die
|
||||
Schnittstelle festgelegt wird, nicht danach.
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
Update-Fenster zuerst, wie besprochen. Es ist klein und klärt die Fenster-Mechanik
|
||||
aus einem Hintergrundfaden — dieselbe Stelle, an der in Etappe 3b der Profilwechsel
|
||||
scheiterte, weil `closeOwnedStages` ein Compose-Fenster nicht erreichte. Das Monitor-
|
||||
Fenster folgt mit dieser Gewissheit im Rücken, sonst käme die Fenster-Mechanik als
|
||||
zweite Fehlerquelle zu den Live-Daten dazu.
|
||||
|
||||
## Aufgaben
|
||||
|
||||
### 1. `UpdateWindowState` mit Tests — **erledigt**
|
||||
|
||||
Umgesetzt in `UpdateWindowState.kt`, neun Testfälle. Beim Lesen kam heraus, dass das
|
||||
Fenster mehr enthält als die Spezifikation andeutet: neben dem Textgitter steht eine
|
||||
`TreeView` mit verborgener Wurzel und zwei Zweigen, „ChangeLog" und „Known bugs".
|
||||
Beide haben **dieselbe Form** — erstes Feld ist die Überschrift, der Rest sind Zeilen —
|
||||
also trägt ein Typ `UpdateSection` beide. Eine allgemeine Baumkomponente braucht es
|
||||
nicht: die Tiefe ist fest zwei.
|
||||
|
||||
Zwei Entscheidungen sind im Quelltext begründet: fehlende Update-Informationen gelten
|
||||
als „kein Update" (die Prüfung läuft beim Start, ein gescheiterter Abruf darf den
|
||||
Start nicht aufhalten), und eine Überschrift ohne Zeilen bleibt erhalten, statt
|
||||
weggefiltert zu werden.
|
||||
|
||||
Die ursprüngliche Beschreibung der Aufgabe:
|
||||
|
||||
|
||||
|
||||
Reine Fachlogik, ohne Oberfläche: Was wird angezeigt, und **wann überhaupt**. Die
|
||||
Bedingung steht heute bei `:9583`: `getLatestVersionNumberOnServer() >
|
||||
APPLICATION_CURRENTVERSIONNUMBER`. Dazu die vier Textfelder aus
|
||||
`getUpdateInformation()` und der Verweis auf die Veröffentlichungsseite.
|
||||
|
||||
Zu prüfen: gleiche Version zeigt nichts, ältere Serverversion zeigt nichts, fehlende
|
||||
Update-Information führt nicht zu einer Ausnahme beim Start.
|
||||
|
||||
### 2. Update-Fenster in Compose — **erledigt und abgenommen**
|
||||
|
||||
Titel „Update information", die sechs Beschriftungen wörtlich. Der Verweis öffnet den
|
||||
Systembrowser. Größe aus `GUIstage_updateStage_SceneSizeHW`.
|
||||
|
||||
**Es folgt dem Design zur Laufzeit**, wie `SettingsWindow` seit Etappe 3b, und es
|
||||
lässt sich von außen schließen. Beides ist keine Kür: ohne das erste steht ein helles
|
||||
Fenster neben einem dunklen, ohne das zweite überlebt es den Profilwechsel.
|
||||
|
||||
**Abgenommen am laufenden Programm**, Tag- und Abenddesign, einschließlich Umschalten
|
||||
zur Laufzeit über „Windows → Use dark mode design". Damit ist auch die neue
|
||||
`applyDarkMode`-Verdrahtung des Update-Fensters belegt.
|
||||
|
||||
### 3. Den JavaFX-Block entfernen und nachmessen — **erledigt**
|
||||
|
||||
`stage_updateStage` samt Feld, `closeOwnedStages`-Eintrag und
|
||||
`registerThemedScene`-Aufruf. Zeilenzahl von `Kst4ContestApplication.java` festhalten
|
||||
(Stand jetzt: 10 904).
|
||||
|
||||
**Ergebnis: 10 904 → 10 802.** Der Block war 145 Zeilen; zurück kamen 36 für
|
||||
`openUpdateWindowIfAvailable`, das nur noch die Entscheidung abfragt und den
|
||||
Systembrowser erreicht.
|
||||
|
||||
**Die Fenstermechanik ist herausgezogen.** `ComposeWindowHost` trägt jetzt das, was
|
||||
jedes Compose-Fenster neben JavaFX braucht: nicht den Prozess beenden, höchstens
|
||||
einmal öffnen, von außen schließbar sein, dem Design folgen. `SettingsWindow` wurde
|
||||
darauf umgestellt und behält seine Schnittstelle zur Java-Seite unverändert. Das war
|
||||
kein Aufräumen um seiner selbst willen: dieselbe Mechanik zum dritten Mal abzuschreiben
|
||||
hätte bedeutet, die `close()`-Verdrahtung zum dritten Mal von Hand zu treffen — und
|
||||
genau deren Fehlen war der Profilwechsel-Fehler aus Etappe 3b.
|
||||
|
||||
`UpdateWindow.close()` und `applyDarkMode` sind in `closeOwnedStages` und `applyTheme`
|
||||
eingetragen, nicht nachgereicht.
|
||||
|
||||
### 4. Die Stationsliste lesen, **bevor** die Tabellenschnittstelle steht — **erledigt**
|
||||
|
||||
**Ergebnis: die drei Tabellen sind ungleich schwer.**
|
||||
|
||||
| Tabelle | Zeilen | Zellfabriken | Zeilenfabrik | Vergleicher |
|
||||
|---|---|---|---|---|
|
||||
| Monitor: DX-Cluster | 183 | 1 | 0 | 0 |
|
||||
| Monitor: QSO der anderen | 222 | 1 | 0 | 0 |
|
||||
| Stationsliste (Etappe 5) | **854** | **13** | **1** | **4** |
|
||||
|
||||
Die beiden Monitor-Tabellen sind schlicht: Text in Spalten, keine Zeilenfärbung, keine
|
||||
eigene Sortierung. Nach ihnen allein zugeschnitten entstünde eine Komponente, die die
|
||||
Stationsliste nicht trägt — **derselbe Fehler wie bei der `DataTable` in Etappe 3b**.
|
||||
|
||||
**Was die Stationsliste zusätzlich verlangt**, aus dem Quelltext gelesen:
|
||||
|
||||
- **22 Spalten**: Callsign, Name, QRA, QRB, QTF, QRG, „AP [minutes / pot%]", Tropo,
|
||||
Score, Act, worked, wkdany, die Bänder 50/70/144/432/23/13/9/6/3, „NOT QRV @",
|
||||
Category.
|
||||
- **Zellinhalt ist nicht gleich Zellenanzeige.** Eine Zellfabrik liest den Wert *und*
|
||||
das Zeilenobjekt *und* Controllerzustand — Beispiel `qraCol`: Kürzen des Textes,
|
||||
Kurzhinweis aus Rasterfeld und „schon gearbeitet", Einfärbung nur wenn der
|
||||
Raster-Knopf aktiv ist.
|
||||
- **Zeilenfärbung nach `getCurrentPriorityScore()`** mit vier Schwellen: über 1000 rot
|
||||
(„imminent sked"), ab 200 orange, ab 100 gelb, 0 und darunter ausgegraut. **Die
|
||||
Farben unterscheiden sich nach Design** — im Quelltext steht heute ein `isDark`-Zweig
|
||||
je Schwelle.
|
||||
- **Eigene Vergleicher** für vier Spalten; die übrigen sortieren nach Text.
|
||||
- **23 Spaltenbreiten** über `TableLayoutManager`, entprellt gespeichert.
|
||||
|
||||
**Daraus der Zuschnitt: die Schnittstelle wird für die Stationsliste entworfen, die
|
||||
Umsetzung nur so weit gebaut, wie Etappe 4 sie braucht.** Eine Spalte ist damit:
|
||||
|
||||
- eine feste Kennung (für die Breitenspeicherung),
|
||||
- eine Überschrift,
|
||||
- eine Funktion Zeile → angezeigter Text,
|
||||
- wahlweise ein Kurzhinweis aus (Zeile, Wert),
|
||||
- wahlweise eine Zellenfärbung aus (Zeile, Wert),
|
||||
- wahlweise ein Vergleicher.
|
||||
|
||||
Die Tabelle nimmt zusätzlich wahlweise eine **Zeilenfärbung** aus der Zeile. So
|
||||
braucht die Stationsliste in Etappe 5 keinen zweiten Zuschnitt, und die
|
||||
Monitor-Tabellen benutzen schlicht die einfachen Fälle.
|
||||
|
||||
Die ursprüngliche Beschreibung der Aufgabe:
|
||||
|
||||
|
||||
`initChatMemberTable` und die Filterleiste aus Etappe 5 durchsehen und aufschreiben,
|
||||
was sie verlangt: Spaltentypen, Sortierung, Zeilenfärbung, Auswahlverhalten,
|
||||
Spaltenbreitenspeicherung. Erst danach die Schnittstelle festlegen.
|
||||
|
||||
Begründung: In Etappe 3b wurde die `DataTable` nach dem Einstellungsfenster
|
||||
zugeschnitten und kann deshalb keine zweite Spalte. Derselbe Fehler ein zweites Mal
|
||||
wäre vermeidbar gewesen.
|
||||
|
||||
### 5. Mehrspaltige Tabelle: Zustand und Regeln, mit Tests — **erledigt**
|
||||
|
||||
`DataTableState` mit zehn Testfällen. Die Zeilenidentität war die Kernentscheidung,
|
||||
und beim Lesen der drei Zeilentypen zeigte sich: **es gibt zwei Sorten Liste, und eine
|
||||
Antwort reicht nicht.**
|
||||
|
||||
| Sorte | Beispiele | Identität |
|
||||
|---|---|---|
|
||||
| Entitätsliste | Stationsliste, gearbeitete Stationen | eigener Schlüssel, `RowKeys.byValue` mit dem rohen Rufzeichen |
|
||||
| Ereignisliste | DX-Cluster, QSO der anderen | `RowKeys.byReference` |
|
||||
|
||||
`ClusterMessage` und `ChatMessage` haben **gar keinen Schlüssel**. Einer aus Zeit,
|
||||
Absender und Text kollidiert, sobald jemand dasselbe Makro zweimal in derselben
|
||||
Sekunde schickt — genau die Falle „Text lässt Dubletten verschmelzen" aus Etappe 2.
|
||||
Zwei gleiche Nachrichten *sind* zwei Nachrichten. `byReference` vergibt Schlüssel beim
|
||||
ersten Sehen und merkt sie sich an der Objektreferenz; das trägt, weil die Roster bei
|
||||
jedem Schnappschuss dieselben Instanzen zurückgeben.
|
||||
|
||||
**Es gibt bewusst keine Vorgabe.** In Etappe 2 kamen die sichtbaren Fehler aus der
|
||||
Zeilenidentität; der Aufrufer zu zwingen, sie zu benennen, hält die Entscheidung dort,
|
||||
wo das Wissen sitzt.
|
||||
|
||||
Zwei weitere Regeln sind festgeschrieben:
|
||||
|
||||
- **Die Auswahl folgt der Zeile, nicht der Position.** Kommt oben etwas dazu, bleibt
|
||||
die gewählte Station dieselbe. Verschwindet sie, wird die Auswahl **gelöscht** statt
|
||||
auf den Nachbarn zu rutschen — sonst zeigt der Operateur auf jemanden, den er nie
|
||||
gewählt hat.
|
||||
- **Die Sortierung ist stabil.** Gleiche Zeilen behalten ihre Ankunftsreihenfolge. Die
|
||||
JavaFX-`SortedList` fügte per Binärsuche ein und legte Gleichstände irgendwohin,
|
||||
weshalb Zeilen sprangen.
|
||||
|
||||
Die ursprüngliche Beschreibung der Aufgabe:
|
||||
|
||||
|
||||
Ohne Oberfläche prüfbar: Spaltendefinition, Sortierung, Auswahl, Zeilenidentität.
|
||||
|
||||
**Die Zeilenidentität ist der kritische Punkt.** In Etappe 2 kamen die Fehler nicht
|
||||
aus der Tabelle, sondern aus der Kopplung zwischen Roster und Anzeige: ein
|
||||
Listenschlüssel darf nicht die Position sein (verschiebt sich beim Einfügen oben) und
|
||||
nicht der Text (fällt bei Dubletten zusammen). Das ist hier erneut zu entscheiden und
|
||||
festzuhalten.
|
||||
|
||||
**Der Zustand hält alles, womit gemalt wird, in Compose-Zustand.** Aus Etappe 3b:
|
||||
gewöhnliche Eigenschaften melden nichts, und bei Live-Daten fällt das nicht als
|
||||
„reagiert nicht" auf, sondern als veraltete Anzeige.
|
||||
|
||||
### 6. Die Tabelle als Compose-Komponente
|
||||
|
||||
Spaltenbreiten werden über die bestehende `TableLayoutManager`-Kennung entprellt
|
||||
gespeichert. Die Kennungen `dx-cluster-monitor` und `qso-other-monitor` bleiben
|
||||
getrennt von denen des Hauptfensters, wie `PROJECT_CONTEXT.md` festlegt.
|
||||
|
||||
### 7. Monitor-Fenster in Compose
|
||||
|
||||
Titel „Cluster & QSO of the other", geteilte Fläche mit den zwei Tabellen, Größe aus
|
||||
`GUIclusterAndQSOMonStage_SceneSizeHW`. Die Teilerposition wird gespeichert wie
|
||||
bisher.
|
||||
|
||||
### 8. Lasttest mit dem Simulator
|
||||
|
||||
Wie in Etappe 2, denn dort hat genau das die Sortier- und Fokusfehler gefunden — kein
|
||||
Test hat sie gesehen. Eine Stunde Dauerlast. Beobachtet wird: springt die Auswahl,
|
||||
kippt die Sortierung, bleibt die Anzeige aktuell, wächst der Speicherbedarf.
|
||||
|
||||
### 9. Den JavaFX-Monitorblock entfernen und nachmessen
|
||||
|
||||
Erst wenn der Lasttest steht.
|
||||
|
||||
## Was unverändert bleiben muss
|
||||
|
||||
- Layout-Kennungen getrennt von denen des Hauptfensters.
|
||||
- `GUIclusterAndQSOMonStage_SceneSizeHW` und `GUIstage_updateStage_SceneSizeHW`
|
||||
behalten Namen und Bedeutung; `configVersion` bleibt 7.
|
||||
- Die Update-Prüfung selbst wird nicht angefasst — nur ihre Anzeige.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
1. ~~**Die macOS-Systemmenüleiste.**~~ **Geklärt.** Compose kennt `MenuBar()` im
|
||||
Geltungsbereich von `Window()`, und auf macOS erscheint sie in der Systemmenüleiste
|
||||
am oberen Bildschirmrand, sobald das Fenster aktiv ist — genau das Verhalten, das
|
||||
`setUseSystemMenuBar(true)` heute erzeugt. Eine *gemeinsame* Leiste über mehrere
|
||||
Fenster gibt es nicht: jedes Fenster erklärt seine eigene.
|
||||
|
||||
Das passt zum heutigen Vorgehen, nur anders herum. `installSharedSystemMenuBar`
|
||||
spiegelt die Menüs des Hauptfensters über `Bindings.bindContent` in eine
|
||||
unsichtbare Leiste jedes Nebenfensters. In Compose wird daraus eine gemeinsame
|
||||
Composable-Funktion, die jedes Fenster aufruft — eine Quelle, fünf Aufrufstellen.
|
||||
|
||||
**Ein Punkt bleibt zu beachten:** die Menüeinträge werden heute aus
|
||||
`onConnectionStateChanged` aktiviert und deaktiviert (`menuItemFileDisconnect`,
|
||||
`menuItemOptionsAwayBack` und weitere). Deren Zustand muss beobachtbar sein, sonst
|
||||
wiederholt sich der Fehler aus Etappe 3b an einer Stelle, an der er besonders
|
||||
schlecht auffällt: ein Menüeintrag, der falsch aktiviert aussieht.
|
||||
|
||||
Quelle: [Menu bar, Kotlin Multiplatform Documentation](https://kotlinlang.org/docs/multiplatform/compose-desktop-menu-bar.html)
|
||||
2. **Entprellte Spaltenbreiten.** `TableLayoutManager` hängt an JavaFX-`TableColumn`.
|
||||
Ob die Compose-Tabelle ihn benutzt oder ein Gegenstück bekommt, entscheidet sich
|
||||
mit Aufgabe 4.
|
||||
@@ -1,91 +0,0 @@
|
||||
# Umsetzungsplan Etappe 5 — Hauptfenster
|
||||
|
||||
Spezifikation: `docs/superpowers/specs/2026-09-25-compose-migration-design.md`, Etappe 5.
|
||||
|
||||
## Der Umfang, gemessen
|
||||
|
||||
| Teil | Zeilen | Besonderheit |
|
||||
|---|---|---|
|
||||
| Stationsliste `initChatMemberTable` | 854 | 22 Spalten, 13 Zellfabriken, 1 Zeilenfabrik, 4 Vergleicher |
|
||||
| Filterleiste | ~977 | 37 Bedienelemente, davon **25 Umschaltknöpfe** |
|
||||
| Infobereich `generateFurtherInfoAbtSelectedCallsignBP` | 881 | eigener Teiler mit gespeicherter Position |
|
||||
| Menüleiste (File, Options, Windows, Help) | ~343 | macOS-Systemmenüleiste |
|
||||
| `TimelineView` | 376 | eigene Datei, eigene Zeichnung |
|
||||
| Top-Prioritätenliste | 69 | |
|
||||
| Statusanzeigen | ~40 | Sked-Warnung, Verbindung, Band-Upgrade |
|
||||
|
||||
Über **3500 Zeilen**. Etappe 3 war kleiner und wurde geteilt.
|
||||
|
||||
**Der Infobereich ist so groß wie die Stationsliste.** In der Spezifikation steht er
|
||||
als einer von sieben Aufzählungspunkten; das untertreibt ihn um eine Größenordnung.
|
||||
|
||||
## Aufteilung in 5a, 5b und 5c
|
||||
|
||||
Nicht aus Ordnungsliebe, sondern weil eine Etappe lieferbar sein soll. Nach jedem
|
||||
Abschnitt läuft die Anwendung, und du kannst abnehmen, bevor der nächste anfängt.
|
||||
|
||||
### 5a — Stationsliste und Filterleiste
|
||||
|
||||
Der Kern. Die mehrspaltige Tabelle aus Etappe 4 wurde **für diese Liste** zugeschnitten
|
||||
und trägt hier ihre erste echte Last: 22 Spalten, Zellfärbung aus Zeile *und* Wert,
|
||||
Zeilenfärbung nach `getCurrentPriorityScore()` über vier Schwellen, eigene Vergleicher,
|
||||
23 gespeicherte Spaltenbreiten.
|
||||
|
||||
Die 25 Umschaltknöpfe der Filterleiste sind kein Formular, sondern Zustand mit Regeln —
|
||||
welche Filter einander ausschließen, welche sich stapeln. Die Regeln gehören in einen
|
||||
testbaren Zustand, bevor ein Knopf gezeichnet wird.
|
||||
|
||||
**Abnahme:** Die Liste zeigt dieselben Stationen in derselben Reihenfolge wie JavaFX,
|
||||
die Filter wirken gleich, Spaltenbreiten überleben den Neustart.
|
||||
|
||||
### 5b — Infobereich, Top-Prioritätenliste, Chat-Reiter
|
||||
|
||||
Der Infobereich hängt an der Auswahl der Liste aus 5a — deshalb danach und nicht
|
||||
davor. Sein Teiler wird hier einmal richtig gebaut; im Monitor-Fenster habe ich ihn
|
||||
bewusst weggelassen, statt ihn zweimal halb zu bauen.
|
||||
|
||||
**Abnahme:** Auswahl in der Liste füllt den Infobereich wie bisher, Teilerpositionen
|
||||
werden entprellt gespeichert.
|
||||
|
||||
### 5c — Menüleiste, Statusleiste, TimelineView, Profilwechsel
|
||||
|
||||
Die Menüleiste wird eine gemeinsame Composable-Funktion, die jedes Fenster aufruft —
|
||||
so verlangt es Compose, und so bekommen die Compose-Fenster auf macOS ihre
|
||||
Systemmenüleiste zurück, die sie seit Etappe 3b nicht haben.
|
||||
|
||||
**Der Profilwechsel ändert seine Natur.** Heute reißt er die Laufzeit ab und baut eine
|
||||
neue `Kst4ContestApplication` auf, weil viele Bedienelemente inline erzeugte
|
||||
Instanzfelder sind. Dieser Grund entfällt; der Wechsel wird zum Austausch des
|
||||
Zustandsobjekts. `shutdownRuntime()` bleibt trotzdem verantwortlich für alles, was eine
|
||||
Verbindung überdauert.
|
||||
|
||||
**Abnahme:** Profilwechsel im laufenden Betrieb, ohne doppelt angemeldete Zuhörer und
|
||||
ohne gehaltene Ressourcen. Auf macOS erscheint die Menüleiste bei jedem Fenster.
|
||||
|
||||
## Was aus Etappe 4 mitgenommen wird
|
||||
|
||||
Vier Fehler dieser Etappe kamen **ausschließlich** durch Hinsehen ans Licht, keiner
|
||||
durch einen Test, und der Prüfer fand nur einen Teil davon:
|
||||
|
||||
1. Ein Zwischenspeicher, der den Zustand nicht mehr las, und eine Tabelle, die deshalb
|
||||
nie wieder zeichnete.
|
||||
2. Eine Kopfzeile, die die ganze Tabellenhöhe nahm.
|
||||
3. Eine Liste, die bei jeder neuen Zeile vom Kopf wegdriftete.
|
||||
4. Ein Menüeintrag, der seinen Umschalter verloren hatte.
|
||||
|
||||
Daraus drei Regeln für diese Etappe:
|
||||
|
||||
- **Was gezeichnet wird, muss beim Lesen Zustand berühren.** Der `Snapshot.observe`-Test
|
||||
aus Etappe 4 fängt genau das und wird hier für jede neue Ansicht angelegt.
|
||||
- **Nach jedem sichtbaren Teil wird gestartet und hingesehen**, nicht erst am Ende des
|
||||
Abschnitts.
|
||||
- **Kein Verhalten wird weggelassen, um ein Darstellungsproblem zu umgehen.** Der
|
||||
verlorene Umschalter war genau dieser Tausch.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
1. Ob `Platform.setImplicitExit(false)` und `ApplicationRuntimeLauncher.exitApplication()`
|
||||
in der Compose-Fassung noch eine Entsprechung brauchen — laut Spezifikation nach
|
||||
Etappe 4 zu klären, also jetzt.
|
||||
2. Ob die Filterleiste ihre 25 Knöpfe behält oder ob mehrere davon derselbe Zustand
|
||||
sind. Vor dem Zeichnen zu lesen, nicht danach.
|
||||
@@ -1,153 +0,0 @@
|
||||
# Umsetzungsplan Etappe 5c, Task 13 — das Hauptfenster zusammensetzen
|
||||
|
||||
Task 13 stand im Etappenplan als **ein** Schritt: „das Hauptfenster zusammengesetzt, das
|
||||
JavaFX-Fenster ersetzt und entfernt". Nach dem Vermessen der Vorlage ist das nicht ein
|
||||
Schritt, sondern fünf. Dieser Plan sagt, welche, und warum die Reihenfolge so ist.
|
||||
|
||||
## Der Baum des JavaFX-Fensters
|
||||
|
||||
`start()` reicht von Zeile 7236 bis 9400 — **2164 Zeilen für ein Fenster**. Was darin
|
||||
steht:
|
||||
|
||||
```
|
||||
Stage (primaryStage) Größe aus GUIscn_ChatwindowMainSceneSizeHW,
|
||||
└─ Scene auf den Schirm begrenzt
|
||||
└─ BorderPane bPaneChatWindow
|
||||
├─ top: FlowPane flwpne_StatusBar ✅ Task 10 + 11
|
||||
└─ center: SplitPane links (waagerecht, 1 Trenner)
|
||||
├─ SplitPane Nachrichtenteil (senkrecht, 4 Trenner)
|
||||
│ ├─ privateMessageTable ❌ fehlt
|
||||
│ ├─ FlowPane flwPane_textSnippets ❌ fehlt
|
||||
│ ├─ VBox pnl_inputAndSendButtons → TimelineView ✅ Task 12
|
||||
│ ├─ HBox textInputFlowPane ❌ fehlt
|
||||
│ └─ TabPane bottomGlobalMessageTabPane ✅ Etappe 5b
|
||||
└─ SplitPane rechts (senkrecht, 2 Trenner)
|
||||
├─ BorderPane chatMemberTableBorderPane ✅ Etappe 5a
|
||||
├─ topPriorityListPane ✅ Etappe 5b
|
||||
└─ selectedCallSignFurtherInfoPane ✅ Etappe 5b
|
||||
```
|
||||
|
||||
Sechs von zehn Bausteinen stehen. Die vier fehlenden sind nicht die kleinen.
|
||||
|
||||
## Was fehlt, mit Zeilen
|
||||
|
||||
| Fehlt | Vorlage | Bemerkung |
|
||||
|---|---|---|
|
||||
| Tabelle der gerichteten Nachrichten | `initChatprivateMSGTable` ab 3730 | eigener Bereich im Splitter, nicht in `MessageTabs` |
|
||||
| Knopfreihe der Textschnipsel | `flwPane_textSnippets` 7890, `buttonFactory` 5233, `refreshShortcutButtons` 4541 | Der **Bestand** liegt schon in `ShortcutsTabState`; nur die Knöpfe fehlen |
|
||||
| Sendezeile | `textInputFlowPane` 7886 | Eingabefeld, `TX`, `Clear`, Trenner, eigene QRG Haupt-/Zweitkategorie, eigener QTF |
|
||||
| Kontextmenüs | `initChatMemberTableContextMenu` 3132, `insertTextSnippet` 7473 | Stationsliste **und** Nachrichtentabellen; Strg+1…Strg+0 für Schnipsel |
|
||||
| Fensterrahmen | `getScreenAwareMainSceneSizeHW` 7178, `ensureStageFitsPrimaryScreen` 9396, `buildOperatorProfileTitleSuffix` 6595 | Größe merken und begrenzen, Titel mit Profilzusatz |
|
||||
| **Splitter mit gemerkten Trennern** | 3 `SplitPane`, 7 Trenner | **Compose hat das nicht** — siehe unten |
|
||||
|
||||
## Das eine Stück, das Compose nicht mitbringt
|
||||
|
||||
`SplitPane` gibt es in Compose Multiplatform nicht. Sieben Trennerstellungen werden
|
||||
heute in den Einstellungen gemerkt:
|
||||
|
||||
```java
|
||||
GUImainWindowLeftSplitPane_dividerposition = {0.51}
|
||||
GUImainWindowRightSplitPane_dividerposition = {0.53, 0.78}
|
||||
GUImessageSectionSplitpane_dividerposition = {0.62, 0.7, 0.75, 0.9}
|
||||
```
|
||||
|
||||
Das ist kein Beiwerk: ein Operator richtet sein Fenster einmal für seinen Schirm ein und
|
||||
erwartet es beim nächsten Contest genauso. Wir brauchen also eine eigene, gezogene
|
||||
Trennleiste mit gemerkter Stellung — dasselbe Muster wie die Spaltengriffe aus Etappe 4
|
||||
(`Density.COLUMN_HANDLE_WIDTH`, Haarlinie in 5 dp Greiffläche), nur senkrecht wie
|
||||
waagerecht und mit Anteilen statt Pixeln.
|
||||
|
||||
`ChatPreferences` liefert die Felder als `double[]`; die Compose-Fassung braucht sie als
|
||||
Zustand, sonst zeichnet das Ziehen nicht mit. Das ist derselbe Fehler wie in Etappe 3b
|
||||
und wird gleich als solcher behandelt.
|
||||
|
||||
## Aufteilung
|
||||
|
||||
### 13a — `SplitterState` und `SplitterPane`
|
||||
|
||||
Ein `n`-teiliger Splitter, waagerecht oder senkrecht, mit `n−1` Trennern als
|
||||
Compose-Zustand, Mindestgrößen pro Bereich und Schreiben nach `ChatPreferences` beim
|
||||
Loslassen (nicht bei jeder Mausbewegung — die JavaFX-Fassung hat bei jedem Pixel
|
||||
`requestLayoutSave()` gerufen und dabei auf die Platte geschrieben).
|
||||
|
||||
**Tests:** Anteile summieren sich zu 1; ein Trenner schiebt keinen anderen vorbei; eine
|
||||
Mindestgröße wird eingehalten; ein abgelehntes Ziehen schreibt nichts (das ist der
|
||||
aufgenommene Kleinmangel aus Etappe 4 bei `resizeColumn`); `Snapshot.observe` auf die
|
||||
Stellungen; ein Bestand mit zu wenigen gemerkten Werten fällt auf die Vorgaben zurück,
|
||||
statt zu stürzen.
|
||||
|
||||
**Abnahme:** Prüfstand mit fünf Bereichen, Ziehen in beiden Richtungen.
|
||||
|
||||
### 13b — Sendezeile und Schnipsel-Knöpfe
|
||||
|
||||
Die Zeile, in der der Operator tatsächlich tippt. `ChatInputState` mit dem Text, dem
|
||||
Ziel (gewählte Station oder Hauptkategorie), den beiden eigenen QRG-Feldern und dem QTF.
|
||||
Darüber die Knopfreihe aus dem Schnipsel-Bestand, die sich neu baut, wenn die
|
||||
Einstellungen sie ändern — `ShortcutsTabState` ruft dafür schon
|
||||
`refreshShortcutButtons`, das heute noch ins Leere zeigt.
|
||||
|
||||
**Tests:** Eingabetaste sendet und leert; `Clear` leert ohne zu senden; die Zielauflösung
|
||||
in der Reihenfolge der Vorlage (gewählte Station → Tabellenauswahl → `ScoreService` →
|
||||
Hauptkategorie); ein Schnipsel wird an der Einfügemarke eingesetzt, nicht angehängt;
|
||||
`Snapshot.observe` auf Text und Ziel.
|
||||
|
||||
**Risiko:** Das Feld hat in JavaFX den Fokus zurückgeholt (`requestFocus`,
|
||||
`selectEnd`) — in Compose ist das ein `FocusRequester` und muss nach jedem Senden und
|
||||
nach jedem Schnipsel wieder greifen. Genau hier hat Etappe 3b Fehler gehabt.
|
||||
|
||||
### 13c — Gerichtete Nachrichten und die Kontextmenüs
|
||||
|
||||
Die vierte Tabelle, mit `DataTableState` wie die anderen, und die Kontextmenüs, die es
|
||||
in Compose noch gar nicht gibt: Rechtsklick auf eine Station (Einträge aus dem
|
||||
gemerkten Bestand), auf eine Nachricht, und Strg+1…Strg+0 für die Schnipsel.
|
||||
|
||||
**Tests:** die Spalten; welche Einträge bei welcher Auswahl erscheinen; dass ein
|
||||
Menüeintrag ohne Auswahl nichts sendet.
|
||||
|
||||
### 13d — Der Fensterrahmen
|
||||
|
||||
Größe aus den Einstellungen, auf die Arbeitsfläche des Schirms begrenzt (die Vorlage tut
|
||||
das, weil ein Fenster von einem größeren Monitor sonst unbedienbar aufgeht), Speichern
|
||||
bei Änderung, Titel aus Chatzustand und Profilzusatz, Schließen über `ComposeWindowHost`.
|
||||
|
||||
**Tests:** die Begrenzungsrechnung mit erfundenen Schirmgrößen (das ist reine Rechnung
|
||||
und gehört nach `core`-Art getestet); der Titel bei gesetztem und fehlendem Profil.
|
||||
|
||||
### 13e — Zusammensetzen, ersetzen, entfernen
|
||||
|
||||
Erst hier entsteht `MainWindow.kt`, und erst hier fällt der JavaFX-Teil. Reihenfolge:
|
||||
|
||||
1. `MainWindow` zusammensetzen, **neben** dem JavaFX-Fenster startbar (beide offen,
|
||||
Vergleich am Bildschirm — so sind in Etappe 4 die Unterschiede aufgefallen).
|
||||
2. Abnahme durch den Operator, mit laufender Verbindung.
|
||||
3. Erst danach `start()` ausräumen und die Zeilenzahl festhalten.
|
||||
|
||||
**Abnahme:** ein Contest-Ablauf von Hand — anmelden, Station wählen, tippen, senden,
|
||||
Sked setzen, Profil wechseln — ohne dass ein Zuhörer doppelt hängt.
|
||||
|
||||
## Reihenfolge und Begründung
|
||||
|
||||
13a zuerst, weil ohne Splitter kein Fenster zusammengeht. 13b vor 13c, weil die
|
||||
Sendezeile das ist, was der Operator die meiste Zeit benutzt; ein Fehler dort wiegt mehr
|
||||
als eine fehlende Tabelle. 13d ist klein und unabhängig. 13e zuletzt und mit beiden
|
||||
Fenstern offen.
|
||||
|
||||
## Was aus Etappe 4 und 5 mitkommt
|
||||
|
||||
- Nach **jedem** sichtbaren Teil starten und hinsehen. Elf Fehler dieser Umstellung sind
|
||||
ausschließlich durch Hinsehen aufgefallen, keiner durch einen Test.
|
||||
- `Snapshot.observe` für jede neue Ansicht.
|
||||
- Kein Verhalten weglassen, um ein Darstellungsproblem zu umgehen.
|
||||
- Vor dem Schreiben die Vorlage lesen, nicht nach dem Schreiben. Die falschen
|
||||
Zuschreibungen dieser Etappe kamen alle daher, dass ich nach dem Namen geraten habe.
|
||||
|
||||
## Offene Entscheidungen
|
||||
|
||||
1. **Die vier Nachrichtenfilter** und **„Always on top"** sind aus Etappe 5b nach 5c
|
||||
verschoben. Gehören sie in 13b (bei der Sendezeile) oder in 13c (bei den Tabellen)?
|
||||
Vorschlag: 13c, weil sie Tabellenfilter sind.
|
||||
2. Der Kleinmangel `GUIpnl_directedMSGWin_dividerposition` ist verwaist — die Vorlage
|
||||
hat nur den `...Default`-Wert. In 13a wegwerfen oder stehen lassen?
|
||||
Vorschlag: wegwerfen, mit einem Satz in der Einbuchung.
|
||||
3. **Der dritte Sortierklick** (Sortierung löschen) fehlt seit Etappe 4. In 13c
|
||||
mitnehmen oder eigene Kleinigkeit?
|
||||
@@ -1,200 +0,0 @@
|
||||
# Umsetzungsplan Etappe 6 — Karte: Angleichung an die Leaflet-Fassung
|
||||
|
||||
Spezifikation: `docs/superpowers/specs/2026-09-25-compose-migration-design.md`, Etappe 6.
|
||||
|
||||
## Was dieser Plan ist — und was nicht
|
||||
|
||||
Etappe 6 hat zwei Hälften: die Karte nach Compose bringen, und JavaFX aus dem Build
|
||||
werfen. Die erste Hälfte wurde bereits angefangen (im Übergabeprotokoll „Task 18/19"),
|
||||
und das Ergebnis trägt nicht. Dieser Plan ist die **Nachbesserung dieser ersten
|
||||
Hälfte**. Das Entfernen von JavaFX und der dafür nötige Ersatz von
|
||||
`JavaFxStylesheet.derive` bleiben ausdrücklich außen vor.
|
||||
|
||||
Der Anlass ist nicht Geschmack. Die Compose-Karte bildet mehrere Mechanismen der
|
||||
Leaflet-Fassung **gar nicht** ab, nicht bloß anders. Drei davon liegen fertig und
|
||||
getestet in `core` und werden nicht aufgerufen.
|
||||
|
||||
## Beweislage
|
||||
|
||||
Verglichen wurde `github_docs/station_map_compact.png` und
|
||||
`github_docs/station_map_path_analysis.png` (JavaFX) gegen den Stand vom 30.09.
|
||||
(`scratch/screenshots/AAA011.png`, `AAA012.png`).
|
||||
|
||||
| # | Befund | Stelle |
|
||||
|---|---|---|
|
||||
| 1 | `MaidenheadGridRenderPlanner` wird nicht aufgerufen — jede Zelle **und jedes Label** wird gezeichnet | `ComposeStationMap.kt:286` gegen `StationMapView.java:1301` |
|
||||
| 2 | Gitterlabel in der Ecke statt in der Zellenmitte, weiß statt `#63067a` | `ComposeStationMap.kt:307` gegen `MapHtmlResources.java:302` |
|
||||
| 3 | Gebrochener Zoom mit `scale()` über die ganze Zeichenfläche; alle Größen werden mit `/ scaleFactor` gegengerechnet | `ComposeStationMap.kt:243` |
|
||||
| 4 | Clustering: 50 px fest, ab 2 Stationen, immer an — statt 55/70/95/125 px je Zoom, ab 3 Stationen, aus ab Zoom 8 | `ComposeStationMap.kt:68` gegen `MapHtmlResources.java:393-410`, `:758` |
|
||||
| 5 | Kopfzeile: nur „Selected: X" statt der sechs Felder; rohe Material-3-Knöpfe statt `Fields.button` | `StationMapWindow.kt:159` gegen `StationMapView.java:571`, `:1010` |
|
||||
| 6 | Zoomknöpfe und OSM-Attribution fehlen | — |
|
||||
| 7 | Kein Kartenhintergrund; fehlende Kacheln zeigen Systemgrau | gegen `MapHtmlResources.java:57`, `:70` |
|
||||
| 8 | `TileFetcher`: abgekündigte a/b/c-Subdomains, nur RAM-Cache, Fehler werden still verschluckt | `TileFetcher.java:44` |
|
||||
| 9 | Absturz „maxWidth must be >= than minWidth", übertüncht durch eine Sperre, die das Fenster unter 500×400 dp leer lässt | `StationMapWindow.kt:151`, `:329` |
|
||||
|
||||
**Der dritte Befund ist die Wurzel von mehreren anderen.** Weil die gesamte
|
||||
Zeichenfläche skaliert wird, hängen Punktradius, Strichstärke und Schriftgröße am
|
||||
Zoom-Bruchteil. In der Leaflet-Fassung war ein Marker **immer** 12 px und ein Label
|
||||
**immer** 12 px, weil Leaflet Overlays in Bildschirmkoordinaten zeichnet. Solange der
|
||||
`scale()`-Block steht, ist jede Farb- und Größenkorrektur nur an einer Zoomstufe
|
||||
richtig — und genau das erklärt, warum mehrere Anläufe nacheinander „fast richtig"
|
||||
aussahen.
|
||||
|
||||
## Getroffene Entscheidungen
|
||||
|
||||
| Frage | Entscheidung |
|
||||
|---|---|
|
||||
| Zielbild | Nah genug an Leaflet, eigene Handschrift in Details erlaubt — gleiche Informationsdichte und Bedienlogik sind Pflicht |
|
||||
| Zoom | Weich zoomen, aber **Overlay in Bildschirmkoordinaten**. Nicht der ganzzahlige Zoom der JavaFX-Fassung |
|
||||
| Absturz | Zuerst reproduzieren und beheben, dann umbauen |
|
||||
| Kacheln | `TileFetcher` reparieren und um einen Plattencache erweitern |
|
||||
|
||||
**Abweichung von der Spezifikation, bewusst.** Die Spezifikation schreibt „Kacheln
|
||||
über den bestehenden `TileProxyServer`". Der Proxy existiert, weil die JavaFX-WebView
|
||||
in AppImage- und Flatpak-Verpackung an TLS scheiterte — ein Compose-Canvas holt die
|
||||
Kacheln selbst und hat dieses Problem nicht. Der Proxy bleibt für den JavaFX-Pfad
|
||||
stehen, bis dieser entfällt.
|
||||
|
||||
**Der ganzzahlige Zoom war ebenfalls ein WebView-Zugeständnis**, nicht Absicht:
|
||||
`MapHtmlResources.java:884` begründet `zoomSnap: 1` mit unzuverlässiger
|
||||
Markerpositionierung in der JavaFX-WebView. Der Grund entfällt mit der WebView.
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
Erst der Absturz, dann die Zeichenebene, dann die Fachregeln, zuletzt die Hülle.
|
||||
Umgekehrt müsste jeder Schritt gegen einen Fehler arbeiten, der ihn jederzeit
|
||||
abbrechen kann.
|
||||
|
||||
## Aufgaben
|
||||
|
||||
### 1. Absturz beheben, Sperre entfernen
|
||||
|
||||
`StationMapWindow.kt:328-330` legt in **eine** `Row` ein `Text` mit fester
|
||||
`Modifier.width(130.dp)` und daneben eine `Box(Modifier.weight(1f))`. Wird das rechte
|
||||
Pane schmaler als diese 130 dp samt Innenabstand, bleibt für das gewichtete Kind eine
|
||||
**negative** Restbreite — und daraus baut Compose die `Constraints`, die mit
|
||||
„maxWidth must be >= than minWidth, maxHeight must be >= than minHeight, minWidth and
|
||||
minHeight must be >= 0" abbrechen.
|
||||
|
||||
Dazu fehlt dem Detailbereich die Mindestbreite, die JavaFX hatte:
|
||||
`detailScrollPane.setMinWidth(210)` und `setPrefWidth(350)`, `StationMapView.java:468`.
|
||||
|
||||
- Reproduzieren: Teiler ganz nach rechts ziehen.
|
||||
- Feste Breite durch `weight` ersetzen, wie es `DetailRow` bereits macht.
|
||||
- Mindestbreite des Detailbereichs im `SplitterState` verankern, nicht im Fenster.
|
||||
- **Erst danach** die Sperre `BoxWithConstraints { if (maxWidth < 500.dp …) return@… }`
|
||||
in Zeile 151 entfernen.
|
||||
|
||||
**Abnahme:** Der Teiler lässt sich über die gesamte Breite ziehen, ohne dass ein
|
||||
Dialog erscheint, und das Fenster bleibt bei jeder Größe gezeichnet.
|
||||
|
||||
### 2. Zeichenebene: Welt- gegen Bildschirmkoordinaten trennen
|
||||
|
||||
Eine Projektionshilfe (Zentrum, Zoom als `Float`, Canvasgröße) rechnet den gebrochenen
|
||||
Zoom über `2^zoom` direkt ein, statt mit `floor(zoom)` zu rechnen und die Zeichenfläche
|
||||
hinterher zu skalieren.
|
||||
|
||||
- Nur die Kachelschicht wird skaliert gezeichnet: Kacheln der Stufe `floor(zoom)`,
|
||||
Zielgröße über den Skalierungsfaktor.
|
||||
- Gitter, Marker, Labels, Antennenkeule, Pfadlinie und Kurzhinweis zeichnen
|
||||
**unskaliert in Bildschirmpixeln**.
|
||||
- `scale()` und sämtliche `/ scaleFactor` entfallen.
|
||||
- Kartenhintergrund `#ede9df` hell, `#23282d` dunkel als erste Zeichenoperation.
|
||||
- Zoombereich 3–18 wie Leaflet; Compose begrenzt heute auf 4–18.
|
||||
|
||||
**Abnahme:** Ein Marker behält seine Größe über den gesamten Zoombereich. Bei
|
||||
abgeschalteten Kacheln ist die Fläche in der Kartenfarbe, nicht in Systemgrau.
|
||||
|
||||
### 3. Gitter über den vorhandenen Planer
|
||||
|
||||
`MaidenheadGridRenderPlanner.createPlan(floor(zoom), bbox, canvasW, canvasH)` aufrufen,
|
||||
Zellen über `buildVisibleCells(…, plan.precision())` holen.
|
||||
|
||||
- Label nur bei `plan.shouldShowLabel(cell)`, Schriftgröße aus `plan.labelFontSizePx()`.
|
||||
- Label **mittig** in der Zelle, in `#63067a` auf halbdurchsichtigem Kästchen.
|
||||
- Linien `#e1e7ec` bei 48 % dunkel, `#46586c` bei 56 % hell, Strichstärke 1.4.
|
||||
|
||||
Der Planer ist vorhanden und wird nicht geändert.
|
||||
|
||||
**Abnahme:** Bei weitem Zoom Zweistellen-Felder, bei mittlerem die vierstelligen
|
||||
Quadrate wie in `station_map_compact.png`; keine Labelteppiche.
|
||||
|
||||
### 4. Clustering nach den Leaflet-Regeln
|
||||
|
||||
`StationMapClusterer` in `core` vom Nächster-Nachbar-Verfahren auf das Bildschirmraster
|
||||
der Leaflet-Fassung umstellen:
|
||||
|
||||
- Zellgröße 55 px ab Zoom 7, 70 px ab 6, 95 px ab 5, darunter 125 px.
|
||||
- Bucket über `floor(bildschirmX / zellgröße)`.
|
||||
- Cluster **erst ab 3** Stationen; darunter werden die Stationen einzeln gezeichnet.
|
||||
- Clustering **aus ab Zoom ≥ 8**.
|
||||
- Ausgewählte Stationen und solche mit `warningToMyDirection` werden **nie** geclustert.
|
||||
- Blasendurchmesser 32 / 36 / 42 px ab 1 / 8 / 20 Stationen.
|
||||
|
||||
Das sind Fachregeln, keine Zeichenregeln — sie gehören mit Tests nach `core`. Die
|
||||
Prüfung der Konstanten gegen `MapHtmlResources.java:393-410` und `:758` ist Teil der
|
||||
Aufgabe.
|
||||
|
||||
**Abnahme:** Tests in `core` decken die vier Zellgrößen, die Schwelle von drei
|
||||
Stationen, die Abschaltung ab Zoom 8 und beide Ausnahmen ab.
|
||||
|
||||
### 5. Kopfzeile und Bedienelemente
|
||||
|
||||
- Statuszeile mit dem vollen Text aus `updateStatusLabel()`
|
||||
(`StationMapView.java:1010`): `Showing N visible stations`, optional
|
||||
`| filtered view active`, `| Selected: …`, Locator, `km / °`, Bänder, QRG.
|
||||
- Statuszeile mit `Modifier.weight(1f)`, damit die Knöpfe rechtsbündig stehen.
|
||||
- `Fields.button` (`FormControls.kt:367`) und `CompactCheckbox` (`:556`) statt roher
|
||||
Material-3-Bedienelemente. Die grünen Pillen entstehen, weil `Theme.kt:97`
|
||||
`primary` auf den Grünton des Abend-Stylesheets legt und Material `Button` genau
|
||||
diese Rolle als Flächenfarbe nimmt.
|
||||
- Zoomknöpfe oben links als Überlagerung.
|
||||
- OSM-Attribution unten rechts. Die ist bei OSM-Kacheln lizenzrechtlich nicht
|
||||
freigestellt.
|
||||
|
||||
**Abnahme:** Die Kopfzeile zeigt dieselben Angaben wie `station_map_compact.png` und
|
||||
sieht aus wie die übrigen Compose-Fenster.
|
||||
|
||||
### 6. Kacheln
|
||||
|
||||
- Host `tile.openstreetmap.org`; die Subdomains `a`/`b`/`c` sind abgekündigt.
|
||||
- Plattencache im Profilverzeichnis, nicht im Arbeitsverzeichnis.
|
||||
- Fehler nach außen melden, damit die Karte „Kacheln nicht erreichbar" anzeigen kann,
|
||||
statt wie in `AAA012.png` kommentarlos grau zu bleiben.
|
||||
- `User-Agent` bleibt wie bisher; die OSM-Nutzungsbedingungen verlangen einen
|
||||
sprechenden Wert.
|
||||
|
||||
**Abnahme:** Nach einem Neustart ohne Netz zeigt die Karte die zuletzt geholten
|
||||
Kacheln aus dem Plattencache und einen Hinweis für die fehlenden.
|
||||
|
||||
## Was unverändert bleibt
|
||||
|
||||
- Der **gesamte JavaFX-Pfad**: `StationMapView.java`, `MapHtmlResources.java`,
|
||||
`StationMapBridge.java`, `TileProxyServer.java`. Sie sind bis zum Entfernen von
|
||||
JavaFX die Referenz.
|
||||
- Die Fachlogik in `core` wird **benutzt, nicht geändert**: `PathAnalysis*`,
|
||||
`Terrain*`, `PathGeometryUtils`, `MapCallsignRawSnapshotBuilder`,
|
||||
`MaidenheadGridUtils`, `MaidenheadGridRenderPlanner`.
|
||||
- `GUIstationMapClusteringEnabled` bleibt eine Layout-Einstellung unterhalb
|
||||
`guiOptions` mit Vorgabewert `true`.
|
||||
- Zurücksetzen der Ansicht lässt den Zoom in Ruhe und löscht nur das Ziel (AGENTS.md).
|
||||
- Die `/cq`-Vorbelegung bei Stationsauswahl.
|
||||
- Der Zoom auf die Bounding Box zwischen eigenem Standort und Zielstation.
|
||||
|
||||
## Abnahmekriterium der Etappe
|
||||
|
||||
Die Karte zeigt dieselben Informationen wie die Leaflet-Fassung. Offline-DEM,
|
||||
Terrain-Pakete und der Profil-Cache funktionieren unverändert.
|
||||
|
||||
Das zweite Kriterium der Spezifikation — keine JavaFX-Abhängigkeit mehr im Projekt —
|
||||
gehört zur zweiten Hälfte der Etappe und ist hier **nicht** erfüllt.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Leistungsverhalten des Canvas bei mehreren tausend Markern. Die Spezifikation nennt
|
||||
als Ausweg eine Vorabaggregation im Kern; Schritt 4 ist genau diese Vorstufe und
|
||||
sollte vor einer Messung fertig sein.
|
||||
- `JavaFxStylesheet.derive` ruft `com.sun.javafx.util.Utils.deriveColor` über
|
||||
Reflexion auf. Das muss **vor** dem Entfernen von JavaFX ersetzt werden, sonst
|
||||
verschieben sich alle abgeleiteten Farben still. Nicht Teil dieses Plans, aber Teil
|
||||
dieser Etappe.
|
||||
@@ -1,506 +0,0 @@
|
||||
# JavaFX entfernen, Teil 1: Bestandsaufnahme und `derive`-Pin
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Den `derive`-Pin so einfrieren, dass er das Entfernen von JavaFX überlebt, und belegen, welcher Teil des nie angezeigten JavaFX-Aufbaus folgenlos gelöscht werden kann.
|
||||
|
||||
**Architecture:** Zwei voneinander unabhängige Stränge. Der erste ersetzt einen Reflexionsaufruf im Test durch eingecheckte Daten — klein, testgetrieben, zeitkritisch. Der zweite ist Verifikationsarbeit an `Kst4ContestApplication`: für jede verdächtige Bindung wird durch Lesen **und** durch einen Lauf der Anwendung festgestellt, ob sie den Betrieb speist oder nur die tote Tabelle. Ergebnis ist ein Befunddokument, auf dem Teil 2 aufsetzt.
|
||||
|
||||
**Tech Stack:** Java 21, Kotlin 2.2, Gradle, JUnit 5, Compose Multiplatform 1.8.2, JavaFX 21 (noch vorhanden).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-01-javafx-entfernen-design.md`
|
||||
|
||||
## Warum nur Teil 1
|
||||
|
||||
Die Spezifikation hat sechs Etappen. Etappe 2 (toten Aufbau löschen), 4 (Dispatcher), 5 (Lebenszyklus) und 6 (Reste und Build) lassen sich heute **nicht ohne Platzhalter** schreiben: welcher Block gelöscht wird und was vorher zu entflechten ist, ist das Ergebnis von Etappe 1. Ein Plan, der das vorwegnimmt, wäre geraten.
|
||||
|
||||
Dieser Plan deckt Etappe 1 und Etappe 3 ab — die beiden, die heute vollständig spezifizierbar sind. Teil 2 wird geschrieben, wenn der Befund aus Aufgabe 5 vorliegt.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Kommunikation mit dem Benutzer auf Deutsch; Quelltextkommentare und Javadoc ausschließlich auf Englisch (`AGENTS.md`).
|
||||
- Keine neue Produktionsabhängigkeit ohne vorherige Freigabe (`AGENTS.md`).
|
||||
- Kein Commit, Push, Merge, Tag oder Release ohne gesonderte ausdrückliche Freigabe (`AGENTS.md`). Die Commit-Schritte unten werden ausgeführt, weil dieser Plan sie benennt und der Benutzer ihn freigibt; alles darüber hinaus nicht.
|
||||
- Der Build erlaubt historisch, dass Testfehler ignoriert werden. **Nicht** aus Exit-Code 0 auf „alle Tests grün" schließen — die XML-Ergebnisse unter `*/build/test-results/` lesen (`AGENTS.md`).
|
||||
- Ausgangslage: `./gradlew clean build` ist grün, 766 Tests, 0 Fehler. Jede Rotfärbung ist ein Signal.
|
||||
- Signierte Commits sind konfiguriert (`commit.gpgsign=true`, SSH). Schlägt das Signieren fehl, läuft der Bitwarden-Agent nicht — das ist kein Grund, unsigniert zu committen.
|
||||
- In diesem Plan wird **keine** Produktionsdatei unter `src/main` gelöscht. Löschen ist Teil 2.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Fünf Dinge, die die Spezifikation voraussetzt, die aber keine Aufgabe hier prüft, geordnet nach Wahrscheinlichkeit, dass sie jemanden treffen:
|
||||
|
||||
1. **Der eingefrorene `derive`-Pin könnte die falschen Werte einfrieren.** Werden die Daten aus der eigenen Umsetzung statt aus JavaFX erzeugt, prüft der Test danach nur noch sich selbst. Aufgabe 1 erzeugt sie deshalb nachweislich über den Reflexionsaufruf und Aufgabe 2 prüft, dass mindestens ein Datensatz von einer absichtlich verfälschten Umsetzung abgelehnt wird.
|
||||
2. **Die eingefrorenen Daten könnten den benutzten Wertebereich nicht abdecken.** `derive` wird mit 26.4 % und 35 % auf Stylesheet-Farben gerufen; Daten nur für Weiß und Schwarz hülfen nicht. Aufgabe 1 deckt die Prozentwerte und Farben ab, die der Test heute benutzt, und zusätzlich die Sprungstellen der Helligkeitsstaffel (0.2, 0.3, 0.4, 0.5, 0.6, 0.85).
|
||||
3. **Der Befund aus Aufgabe 5 könnte eine Bindung übersehen, die nur bei bestimmten Einstellungen lebt.** Ein Profil mit anderen aktiven Bändern oder abgeschalteter Karte durchläuft andere Zweige. Aufgabe 4 prüft deshalb gegen ein zweites Profil.
|
||||
4. **Ein Listener könnte nur beim Profilwechsel lebendig werden.** `closeOwnedStages` und der Wiederaufbau laufen nur dort. Aufgabe 4 schließt einen Profilwechsel ein.
|
||||
5. **Die Systemmenüleiste könnte auf macOS anders leben als auf Linux.** Geprüft wird auf Linux; der Befund hält das ausdrücklich als ungeprüft fest, statt es offenzulassen.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Datei | Verantwortung | Aufgabe |
|
||||
|---|---|---|
|
||||
| `app-desktop/src/test/resources/javafx-derive-reference.txt` | **neu** — die von JavaFX gelieferten Werte als eingecheckte Daten | 1 |
|
||||
| `app-desktop/src/test/kotlin/kst4contest/view/compose/JavaFxStylesheetTest.kt` | **ändern** — vergleicht gegen die Daten statt gegen den Reflexionsaufruf | 1, 2 |
|
||||
| `app-desktop/src/main/kotlin/kst4contest/view/compose/JavaFxStylesheet.kt` | **ändern** — nur der veraltete Kommentar | 2 |
|
||||
| `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md` | **neu** — der Befund, auf dem Teil 2 aufsetzt | 3, 4, 5 |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Die JavaFX-Werte als Daten einfrieren
|
||||
|
||||
**Files:**
|
||||
- Create: `app-desktop/src/test/resources/javafx-derive-reference.txt`
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/compose/JavaFxStylesheetTest.kt`
|
||||
- Test: dieselbe Datei
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `JavaFxStylesheet.derive(color: Color, percent: Double): Color` — unverändert.
|
||||
- Produces: die Ressource `javafx-derive-reference.txt`, Zeilenformat
|
||||
`<rgbHex>;<percent>;<erwarteterRgbHex>`, z. B. `#ececec;26.4;#f4f4f4`. Drei
|
||||
Kommentarzeilen am Anfang, jede mit `#`. Aufgabe 2 schreibt den Leser dafür.
|
||||
Ausserdem bleibt `deriveWithJavaFx(rgb: String, percent: Double): String` im Test
|
||||
erhalten — Aufgabe 2 entfernt nur seinen Aufruf aus dem laufenden Vergleich, nicht
|
||||
die Methode.
|
||||
|
||||
- [ ] **Step 1: Den bestehenden Test lesen, bevor etwas geändert wird**
|
||||
|
||||
Run: `sed -n '1,60p;160,180p' app-desktop/src/test/kotlin/kst4contest/view/compose/JavaFxStylesheetTest.kt`
|
||||
|
||||
Erwartet: `deriveWithJavaFx(rgb, percent)` ruft über `Class.forName("com.sun.javafx.util.Utils").getMethod("deriveColor", ...)` das echte JavaFX. Der Test `derive gives the values JavaFX gives` vergleicht drei Größen: die erwarteten Werte aus der Tabelle, `JavaFxStylesheet.derive` und `deriveWithJavaFx`. Notiere den genauen Namen der Hilfsmethoden `parse`, `hex` und `assertChannelsClose` — sie werden unten weiterbenutzt.
|
||||
|
||||
- [ ] **Step 2: Einen Generator schreiben, der die Daten aus JavaFX erzeugt**
|
||||
|
||||
Ein JUnit-Test, der die Datei schreibt statt etwas zu behaupten. Er wird einmal benutzt und bleibt danach als `@Disabled` stehen, damit nachvollziehbar ist, woher die Daten kommen.
|
||||
|
||||
In `JavaFxStylesheetTest.kt` ergänzen:
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Writes the reference data the frozen test compares against.
|
||||
*
|
||||
* Disabled because it is a generator, not an assertion: it runs once, while JavaFX
|
||||
* is still on the classpath, and its output is checked in. Kept rather than deleted
|
||||
* so that the provenance of the data is readable — the values come from
|
||||
* com.sun.javafx.util.Utils.deriveColor and from nowhere else, which is the whole
|
||||
* point of pinning them.
|
||||
*
|
||||
* To regenerate: remove @Disabled, run, restore @Disabled.
|
||||
*/
|
||||
@org.junit.jupiter.api.Disabled("generator; run by hand while JavaFX is present")
|
||||
@Test
|
||||
fun `generate the JavaFX reference data`() {
|
||||
val colours = listOf(
|
||||
"#ffffff", "#000000", "#ececec", "#373e43", "#1d1d1d", "#4da6ff",
|
||||
"#2b2b2b", "#808080", "#333333", "#4d4d4d", "#666666", "#999999",
|
||||
"#d9d9d9", "#f4f4f4", "#63067a", "#ff9900",
|
||||
)
|
||||
val percents = listOf(-80.0, -50.0, -35.0, -30.0, -10.0, 10.0, 26.4, 35.0, 50.0, 80.0)
|
||||
|
||||
val lines = buildList {
|
||||
for (rgb in colours) {
|
||||
for (percent in percents) {
|
||||
add("$rgb;$percent;${deriveWithJavaFx(rgb, percent)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val target = java.io.File("src/test/resources/javafx-derive-reference.txt")
|
||||
target.parentFile.mkdirs()
|
||||
target.writeText(
|
||||
"# Generated from com.sun.javafx.util.Utils.deriveColor by\n" +
|
||||
"# JavaFxStylesheetTest.`generate the JavaFX reference data`.\n" +
|
||||
"# Format: <rgbHex>;<percent>;<expectedRgbHex>\n" +
|
||||
lines.joinToString("\n") + "\n"
|
||||
)
|
||||
println("wrote ${lines.size} pairs to ${target.absolutePath}")
|
||||
}
|
||||
```
|
||||
|
||||
Die Farbliste deckt ab: Weiß und Schwarz (Ränder), die Modena-Basis `#ececec`, die Abendblech-Basis `#373e43`, Marker- und Gitterfarben aus der Karte, und Graustufen, die die Sprungstellen der Helligkeitsstaffel treffen (0.2, 0.3, 0.4, 0.5, 0.6, 0.85 — siehe Review Focus 2). Die Prozentwerte decken die im Produktionscode benutzten 26.4 und 35 ab, dazu negative und positive Extreme.
|
||||
|
||||
- [ ] **Step 3: Den Generator einmal laufen lassen**
|
||||
|
||||
`@Disabled` vorübergehend auskommentieren, dann:
|
||||
|
||||
Run: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.JavaFxStylesheetTest' --console=plain -i 2>&1 | grep "wrote"`
|
||||
|
||||
Erwartet: eine Zeile `wrote 160 pairs to /…/app-desktop/src/test/resources/javafx-derive-reference.txt`.
|
||||
|
||||
Danach `@Disabled` wieder einkommentieren.
|
||||
|
||||
- [ ] **Step 4: Prüfen, dass die Daten wirklich von JavaFX stammen**
|
||||
|
||||
Run: `head -5 app-desktop/src/test/resources/javafx-derive-reference.txt && wc -l app-desktop/src/test/resources/javafx-derive-reference.txt`
|
||||
|
||||
Erwartet: drei Kommentarzeilen, dann Zeilen der Form `#ffffff;-80.0;#333333`. Insgesamt 163 Zeilen.
|
||||
|
||||
Stichprobe von Hand: `#ffffff;-30.0;…` muss ein mittleres Grau sein, nicht Weiß. Ist die Spalte identisch mit der ersten, hat der Generator nicht JavaFX gefragt — dann Schritt 2 prüfen, **nicht** weitermachen.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app-desktop/src/test/resources/javafx-derive-reference.txt \
|
||||
app-desktop/src/test/kotlin/kst4contest/view/compose/JavaFxStylesheetTest.kt
|
||||
git commit -m "Freeze the JavaFX colour-derivation reference as test data
|
||||
|
||||
JavaFxStylesheetTest compares derive() against com.sun.javafx.util.Utils
|
||||
at runtime, through reflection. That comparison stops working the moment
|
||||
JavaFX leaves the classpath, and the test would then quietly fall back to
|
||||
checking derive() against itself.
|
||||
|
||||
The pairs are generated from the real JavaFX implementation while it is
|
||||
still here and checked in. The generator stays, disabled, so the
|
||||
provenance of the numbers is readable."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Den Test gegen die Daten prüfen lassen
|
||||
|
||||
**Files:**
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/compose/JavaFxStylesheetTest.kt`
|
||||
- Modify: `app-desktop/src/main/kotlin/kst4contest/view/compose/JavaFxStylesheet.kt` (nur Kommentar)
|
||||
- Test: dieselbe Testdatei
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `javafx-derive-reference.txt` aus Aufgabe 1.
|
||||
- Produces: nichts, was spätere Aufgaben benutzen.
|
||||
|
||||
- [ ] **Step 1: Den Test schreiben, der gegen die Daten prüft**
|
||||
|
||||
In `JavaFxStylesheetTest.kt` ergänzen:
|
||||
|
||||
```kotlin
|
||||
/** The reference pairs, as `(rgbHex, percent, expectedRgbHex)`. */
|
||||
private fun referencePairs(): List<Triple<String, Double, String>> {
|
||||
val resource = javaClass.getResourceAsStream("/javafx-derive-reference.txt")
|
||||
?: error("javafx-derive-reference.txt is missing from the test resources")
|
||||
|
||||
return resource.bufferedReader().readLines()
|
||||
.filterNot { it.isBlank() || it.startsWith("#") }
|
||||
.map { line ->
|
||||
val (rgb, percent, expected) = line.split(";")
|
||||
Triple(rgb, percent.toDouble(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* derive() against the values JavaFX itself produced.
|
||||
*
|
||||
* Against checked-in data rather than a live reflection call, because the live call
|
||||
* disappears with JavaFX and would take the only real check with it — leaving a
|
||||
* test that compares derive() to derive().
|
||||
*/
|
||||
@Test
|
||||
fun `derive matches the frozen JavaFX reference`() {
|
||||
val pairs = referencePairs()
|
||||
|
||||
assertTrue(pairs.size >= 100) { "only ${pairs.size} reference pairs; the file looks truncated" }
|
||||
|
||||
pairs.forEach { (rgb, percent, expected) ->
|
||||
assertChannelsClose(
|
||||
parse(expected),
|
||||
JavaFxStylesheet.derive(parse(rgb), percent),
|
||||
"derive($rgb, $percent%)",
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Den Test laufen lassen und sehen, dass er grün ist**
|
||||
|
||||
Run: `./gradlew :app-desktop:test --tests 'kst4contest.view.compose.JavaFxStylesheetTest' --console=plain > /tmp/t1.txt 2>&1; python3 -c "
|
||||
import glob,xml.etree.ElementTree as ET
|
||||
for f in glob.glob('app-desktop/build/test-results/test/TEST-*JavaFxStylesheetTest.xml'):
|
||||
r=ET.parse(f).getroot(); print(f\"tests={r.get('tests')} failures={r.get('failures')} skipped={r.get('skipped')}\")
|
||||
for tc in r.iter('testcase'):
|
||||
for b in list(tc.iter('failure'))+list(tc.iter('error')): print(tc.get('name'), (b.get('message') or '')[:200])"`
|
||||
|
||||
Erwartet: `failures=0`, `skipped=1` (der Generator).
|
||||
|
||||
Ein grüner Test ist hier **noch kein Beweis** — er könnte grün sein, weil die Daten aus der eigenen Umsetzung stammen. Das prüft der nächste Schritt.
|
||||
|
||||
- [ ] **Step 3: Beweisen, dass der Test den Fehler fängt, den er fangen soll**
|
||||
|
||||
Review Focus 1. Die Produktionsumsetzung vorübergehend verfälschen:
|
||||
|
||||
In `JavaFxStylesheet.kt`, in `derive`, `calcBrightness = calcBrightness.coerceIn(-1.0, 1.0)` ersetzen durch `calcBrightness = (calcBrightness * 1.05).coerceIn(-1.0, 1.0)`.
|
||||
|
||||
Run: denselben Befehl wie Schritt 2.
|
||||
|
||||
Erwartet: `failures=1`, und die Meldung nennt ein konkretes `derive(#…, …%)`. Fällt der Test **nicht**, sind die Daten wertlos — dann stammen sie nicht von JavaFX; zurück zu Aufgabe 1.
|
||||
|
||||
Danach die Verfälschung zurücknehmen und erneut laufen lassen: `failures=0`.
|
||||
|
||||
- [ ] **Step 4: Den Reflexionsvergleich aus dem laufenden Test nehmen**
|
||||
|
||||
Die Zeile, die `deriveWithJavaFx` im Test `derive gives the values JavaFX gives` aufruft (um `:50`), entfernen, sodass dieser Test nur noch seine eigene Tabelle gegen `JavaFxStylesheet.derive` prüft. `deriveWithJavaFx` selbst **bleibt** — der Generator in Aufgabe 1 braucht es.
|
||||
|
||||
Begründung als Kommentar über den entfernten Vergleich setzen:
|
||||
|
||||
```kotlin
|
||||
/*
|
||||
* No live JavaFX comparison here. It lives in the frozen reference data
|
||||
* instead (javafx-derive-reference.txt), because a reflection call into
|
||||
* com.sun.javafx stops working when JavaFX is removed and would take the
|
||||
* only real check with it.
|
||||
*/
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Den veralteten Kommentar in der Produktionsdatei berichtigen**
|
||||
|
||||
In `JavaFxStylesheet.kt` beschreibt der Kommentar über `derive` (um `:116`–`:129`) eine Reflexionsfassung, die es nicht mehr gibt. Ersetzen durch:
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* JavaFX `derive(colour, percent)`, reimplemented.
|
||||
*
|
||||
* No JavaFX call and no reflection: this is the algorithm itself. The brightness it
|
||||
* derives from is not the HSB value but `0.3R + 0.59G + 0.11B`, and the positive
|
||||
* branch follows a staircase rather than a formula, which is why an earlier
|
||||
* approximation here was measurably wrong.
|
||||
*
|
||||
* Correctness is not argued, it is pinned: `JavaFxStylesheetTest` checks this
|
||||
* against values the real `com.sun.javafx.util.Utils.deriveColor` produced, frozen
|
||||
* into `javafx-derive-reference.txt` so the check survives JavaFX being removed.
|
||||
*/
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Die ganze Suite laufen lassen**
|
||||
|
||||
Run: `./gradlew clean build --console=plain > /tmp/b.txt 2>&1; echo "exit=$?"; python3 -c "
|
||||
import glob,xml.etree.ElementTree as ET
|
||||
t=f=0
|
||||
for p in glob.glob('*/build/test-results/test/TEST-*.xml'):
|
||||
r=ET.parse(p).getroot(); t+=int(r.get('tests')); f+=int(r.get('failures'))+int(r.get('errors'))
|
||||
print(f'tests={t} failures={f}')"`
|
||||
|
||||
Erwartet: `exit=0`, `failures=0`, und `tests` um eins höher als die 766 vom Ausgangsstand.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add app-desktop/src/test/kotlin/kst4contest/view/compose/JavaFxStylesheetTest.kt \
|
||||
app-desktop/src/main/kotlin/kst4contest/view/compose/JavaFxStylesheet.kt
|
||||
git commit -m "Check derive() against the frozen reference instead of live JavaFX
|
||||
|
||||
The reflection call stays only in the disabled generator. The running
|
||||
test now reads checked-in values, so it keeps its meaning once JavaFX is
|
||||
gone rather than silently comparing derive() with itself.
|
||||
|
||||
Verified by breaking derive() on purpose and watching the test fail.
|
||||
|
||||
Also corrects the doc comment on derive(), which still described a
|
||||
reflection delegation that the code has not done for some time."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Feststellen, ob `FxRosterBinding` den Betrieb speist
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
|
||||
- Read only: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`, `app-desktop/src/main/java/kst4contest/view/FxRosterBinding.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nichts.
|
||||
- Produces: das Befunddokument mit dem Abschnitt „FxRosterBinding". Aufgabe 5 fasst zusammen, Teil 2 baut darauf auf.
|
||||
|
||||
**Keine Produktionsdatei wird in dieser Aufgabe geändert.**
|
||||
|
||||
- [ ] **Step 1: Alle Nutzungen des Spiegels auflisten**
|
||||
|
||||
Run: `grep -n "chatMemberListBinding" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
|
||||
|
||||
Erwartet: Erzeugung um `:2513`, Nullsetzung um `:6877`, eine Prüfung um `:1766`. Jede weitere Fundstelle notieren.
|
||||
|
||||
- [ ] **Step 2: Feststellen, wer den gespiegelten `ObservableList` liest**
|
||||
|
||||
Run: `grep -n "chatMemberListBinding.get\|chatMemberListBinding\.\(mirror\|list\|items\)" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java; grep -n "public" app-desktop/src/main/java/kst4contest/view/FxRosterBinding.java`
|
||||
|
||||
Erwartet: die öffentliche Fläche von `FxRosterBinding` und jede Stelle, die den Spiegel abruft. Für jede Stelle festhalten: landet sie in einer `TableView` (tot) oder in etwas, das Compose liest (lebend)?
|
||||
|
||||
- [ ] **Step 3: Feststellen, woher die Compose-Stationsliste ihre Zeilen bekommt**
|
||||
|
||||
Run: `grep -n "DataTableState<ChatMember>\|setRows\|rows =" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java | head -20; grep -n "fun setRows\|var rows\|val rows" app-desktop/src/main/kotlin/kst4contest/view/compose/DataTableState.kt`
|
||||
|
||||
Erwartet: eine durchgehende Kette von `ObservableRoster` bis `DataTableState.rows`. Notiere sie Glied für Glied. Führt die Kette durch `chatMemberListBinding`, ist der Spiegel **lebend**.
|
||||
|
||||
- [ ] **Step 4: Den Befund schreiben**
|
||||
|
||||
`docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md` anlegen:
|
||||
|
||||
```markdown
|
||||
# Lebendbefund: was im JavaFX-Aufbau von KST4Contest tatsächlich benutzt wird
|
||||
|
||||
Grundlage für Teil 2 von `docs/superpowers/specs/2026-10-01-javafx-entfernen-design.md`.
|
||||
Erhoben am 1. Oktober 2026 auf Linux, Branch `nextMajorRelease/version1_50`.
|
||||
|
||||
Jeder Block bekommt genau einen von zwei Töpfen:
|
||||
**folgenlos löschbar** oder **zu entflechten** (mit der Stelle, die davon abhängt).
|
||||
|
||||
## FxRosterBinding (`chatMemberListBinding`)
|
||||
|
||||
**Topf:** <folgenlos löschbar | zu entflechten>
|
||||
|
||||
**Belege:**
|
||||
- Erzeugt bei `Kst4ContestApplication.java:<Zeile>`, genullt bei `:<Zeile>`.
|
||||
- Gelesen von: <Liste der Stellen, je mit Zeile und „tot/lebend">
|
||||
- Die Compose-Stationsliste bezieht ihre Zeilen über: <die Kette aus Schritt 3>
|
||||
|
||||
**Folgerung:** <ein Satz>
|
||||
```
|
||||
|
||||
Jede spitze Klammer wird durch das ersetzt, was die Schritte 1–3 ergeben haben. Bleibt eine stehen, ist die Aufgabe nicht fertig.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
|
||||
git commit -m "Record what FxRosterBinding actually feeds
|
||||
|
||||
First section of the finding that part 2 of the JavaFX removal builds on.
|
||||
No production code touched."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Die übrigen Verdachtsfälle am laufenden Programm prüfen
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
|
||||
- Read only: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: das Befunddokument aus Aufgabe 3.
|
||||
- Produces: dasselbe Dokument mit den Abschnitten „registerThemedScene", „LayoutAutosave", „Systemmenüleiste" und „Kandidatenfenster".
|
||||
|
||||
**Keine Produktionsdatei wird in dieser Aufgabe geändert.** Diese Aufgabe erfordert, die Anwendung zu starten und zu bedienen; Lesen allein beantwortet sie nicht.
|
||||
|
||||
- [ ] **Step 1: Die Stellen auflisten**
|
||||
|
||||
Run: `grep -n "registerThemedScene\|LayoutAutosave\|layoutAutosave\|installSharedSystemMenuBar\|initMenuBar\|showTopPriorityCandidatesWindow" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
|
||||
|
||||
Erwartet unter anderem: drei Aufrufe von `registerThemedScene`, `layoutAutosave` erzeugt um `:7420`, `installSharedSystemMenuBar(candidatesScene)` um `:4858`, `initMenuBar()` um `:5729`, `showTopPriorityCandidatesWindow` gerufen bei `:4706` (toter JavaFX-Knopf) und `:10707` (lebender Compose-Pfad).
|
||||
|
||||
- [ ] **Step 2: Die Anwendung starten und das Kandidatenfenster öffnen**
|
||||
|
||||
Run: `./gradlew :app-desktop:run`
|
||||
|
||||
Dann im Compose-Hauptfenster den Knopf **more** neben den Prioritätsknöpfen drücken.
|
||||
|
||||
Erwartet: ein Fenster mit der Kandidatenliste erscheint. Festhalten: Titelleiste, ob es eine eigene Menüleiste trägt, ob es modal ist, und ob ein Klick auf einen Eintrag im Hauptfenster eine Station auswählt.
|
||||
|
||||
Erscheint **kein** Fenster, ist der Pfad trotz `:10707` tot — das ist ein genauso wertvoller Befund und gehört so ins Dokument.
|
||||
|
||||
- [ ] **Step 3: Das Design umschalten und beobachten, ob die Compose-Fenster folgen**
|
||||
|
||||
Bei laufender Anwendung das Design über die Menüleiste des Hauptfensters umschalten (Tag/Abend).
|
||||
|
||||
Erwartet: Hauptfenster, Kartenfenster und Einstellungsfenster wechseln die Farben. Notieren, ob **alle** folgen. Folgt eines nicht, hängt es womöglich an `registerThemedScene` und gehört in den Topf „zu entflechten".
|
||||
|
||||
- [ ] **Step 4: Einen Profilwechsel durchführen**
|
||||
|
||||
Review Focus 4. Über die Menüleiste das Bedienerprofil wechseln.
|
||||
|
||||
Erwartet: die Fenster werden geschlossen und neu aufgebaut; die Anwendung bleibt bedienbar; in der Konsole erscheint keine Ausnahme. Notieren, was passiert.
|
||||
|
||||
- [ ] **Step 5: Mit einem zweiten Profil wiederholen**
|
||||
|
||||
Review Focus 3. Ein Profil mit anderen aktiven Bändern wählen (mindestens ein Band an- oder abgeschaltet gegenüber dem ersten) und die Schritte 2 bis 4 wiederholen.
|
||||
|
||||
Erwartet: dasselbe Verhalten. Weicht es ab, gehört die Abweichung mit der Einstellung ins Dokument, die sie auslöst.
|
||||
|
||||
- [ ] **Step 6: Fenstergrößen auf Persistenz prüfen**
|
||||
|
||||
Ein Fenster in der Größe ändern, die Anwendung beenden, neu starten.
|
||||
|
||||
Erwartet: die Größe ist erhalten. Ist sie es nicht, schreibt `LayoutAutosave` womöglich Werte, die niemand mehr liest — oder umgekehrt. Notieren.
|
||||
|
||||
- [ ] **Step 7: Die Abschnitte ins Befunddokument schreiben**
|
||||
|
||||
Für jeden der vier Blöcke — `registerThemedScene`, `LayoutAutosave`, Systemmenüleiste, Kandidatenfenster — denselben Aufbau wie in Aufgabe 3 Schritt 4: Topf, Belege, Folgerung.
|
||||
|
||||
Die Systemmenüleiste bekommt zusätzlich die Zeile:
|
||||
|
||||
```markdown
|
||||
**Nicht geprüft:** macOS. Erhoben wurde auf Linux. Ob die Systemmenüleiste dort
|
||||
sichtbar an der JavaFX-`MenuBar` hängt, ist offen und muss vor dem Entfernen auf
|
||||
einem Mac nachgeholt werden.
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
|
||||
git commit -m "Record what the themed scenes, autosave and menu bar actually do
|
||||
|
||||
Established by running the application and operating it, not by reading:
|
||||
a listener that is never fired reads the same as one that does not exist.
|
||||
Includes a profile switch and a second profile, and marks macOS as
|
||||
unverified. No production code touched."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Den Befund abschließen und Teil 2 umreißen
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: alle Abschnitte aus Aufgabe 3 und 4.
|
||||
- Produces: die Löschreihenfolge, aus der Teil 2 geschrieben wird.
|
||||
|
||||
- [ ] **Step 1: Den toten Aufbau in Scheiben zerlegen**
|
||||
|
||||
Run: `grep -n "private.*void init[A-Z][a-zA-Z]*(\|private.*Pane init[A-Z][a-zA-Z]*(\|private.*Node init[A-Z][a-zA-Z]*(" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
|
||||
|
||||
Erwartet: die `init…`-Methoden, die zusammen das nie gezeigte Fenster bauen. Für jede notieren: Zeilenbereich, ungefähre Länge und ob der Befund aus Aufgabe 3 und 4 sie als „folgenlos löschbar" ausweist.
|
||||
|
||||
- [ ] **Step 2: Eine Reihenfolge festlegen, die den Build nach jeder Scheibe grün lässt**
|
||||
|
||||
Blätter zuerst, Wurzeln zuletzt: eine Methode, die niemand mehr ruft, lässt sich löschen; eine, die noch gerufen wird, nicht. Die Reihenfolge als nummerierte Liste ins Dokument schreiben, jede Scheibe mit den Methoden, die sie umfasst.
|
||||
|
||||
- [ ] **Step 3: Die Zusammenfassung schreiben**
|
||||
|
||||
Am Anfang des Dokuments, unter der Überschrift:
|
||||
|
||||
```markdown
|
||||
## Ergebnis in einem Satz
|
||||
|
||||
<Von den N init-Methoden sind M folgenlos löschbar; K sind zu entflechten, nämlich …>
|
||||
|
||||
## Löschreihenfolge für Teil 2
|
||||
|
||||
1. <Scheibe> — <Methoden> — <geschätzte Zeilen>
|
||||
2. …
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Prüfen, dass keine Lücke bleibt**
|
||||
|
||||
Run: `grep -c "zu entflechten\|folgenlos löschbar" docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md; grep -n "<" docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
|
||||
|
||||
Erwartet: jeder Block hat genau einen Topf, und der zweite Befehl findet **keine** spitze Klammer mehr. Findet er eine, ist eine Vorlage nicht ausgefüllt.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
|
||||
git commit -m "Finish the live-code finding and derive the deletion order
|
||||
|
||||
Part 2 of the JavaFX removal is written from this, not from assumption."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Was dieser Plan nicht tut
|
||||
|
||||
- Er löscht keine Produktionsdatei.
|
||||
- Er fasst den `UiDispatcher`, den Lebenszyklus und die zehn Dialoge nicht an.
|
||||
- Er entfernt `org.openjfx` nicht aus dem Build.
|
||||
|
||||
Das ist Teil 2, und der wird geschrieben, wenn der Befund aus Aufgabe 5 vorliegt.
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user