Files
kst4contest/docs/superpowers/plans/2026-10-02-javafx-entfernen-teil3.md
T
Rsclub2_2 fe4215bce9 Remove JavaFX
The second half of Etappe 6 (61 commits). grep -rl "javafx\." over both main
source trees is empty, nothing imports javafx, neither classpath resolves it,
and org.openjfx is out of the build and the version catalog.

The finding that shaped the work: the JavaFX main window was built on every
start and never shown -- primaryStage's setScene and show were both commented
out. Most of those 11,655 lines were dead weight rather than a port. But not
all of them, and that is the trap this commit is really about: dead-looking
methods still fed live Compose sinks. They were cut at their seams instead of
deleted, and a sweep script that follows calls was written to find them,
because reading the code does not reveal them.

In order: the colour derivation frozen as checked-in test data, so the pin
survives JavaFX being removed instead of silently comparing against itself;
the live feeds lifted out of the never-shown window and given names and tests;
the dead scaffolding deleted in slices; then the lifecycle. UiDispatcher now
delivers to the AWT event dispatch thread, the ten Alerts are modal Compose
dialogs, HostServices gives way to java.awt.Desktop with the mailto/http split
JavaFX did not need, and extends Application, start(Stage), stop and launch are
gone -- main builds the runtime and a shutdown hook replaces stop().

An independent review found two deadlocks in that move, both the same failure
class, both invisible to the build: a blocking wait on the event thread for
work that can only advance on the event thread. The profile picker waited on a
latch and froze the client for good; ComposeWindowHost.close waited on
Thread.join and returned with the window still marked open, which made a
profile switch raise the dead runtime's window and leave the rebuilt profile
without its Connect button. A third came out of fixing them: a ComposeDialog
built off the event thread cannot create its GL context and never paints.

Three defects only a GUI run showed, never the build: an NPE on a deleted
field, a Compose menu still hanging on deleted MenuItems, and a category
bookkeeping field that looked dead but is read by the outgoing-message category
resolution -- deleting it would have sent messages in the wrong category.

The binding docs and the manual no longer describe a JavaFX client.
2026-10-05 22:36:01 +02:00

1413 lines
70 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# JavaFX entfernen, Teil 3: Entlang der Schnittlinien trennen und den durchsetzten Aufbau löschen
> **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:** Die drei gemischten Methoden, die Teil 2 mit einer Schnittlinie markiert hat, entlang dieser Linie trennen — der lebende Compose-Schreibweg bleibt, der nie gezeigte JavaFX-Teil fällt — und danach den gesamten durchsetzten Aufbau in `Kst4ContestApplication` in bauenden, geprüften Scheiben löschen, bis die Klasse auf ihre lebende Fläche zusammengeschrumpft ist.
**Architecture:** Teil 2 hat die harte Arbeit vorweggenommen: die drei Zuführungen (`TimelineFeed`, `SelectedStationMessagesFeed`, `ConnectionStateFeed`) existieren als benannte, getestete Einheiten, werden neben der Deklaration von `composeMainWindowState` erzeugt (nicht mehr verstreut in `start()`), und drei Methoden tragen den Kommentar „part 3 cuts at this line". Teil 3 ist deshalb kein Entwurf, sondern eine Ausführung: entlang gesetzter Linien schneiden, dann Blätter vor Wurzeln löschen. Jede Löschscheibe wird einzeln gebaut, mit dem Durchlauf-Skript vorgeprüft und mit einem Anwendungsstart abgenommen, weil kein Test beantwortet, ob ein entfernter Listener gefehlt hat.
**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`
**Befund, auf dem jede Aufgabe aufsetzt:** `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
## Was Teil 2 geliefert hat — und was dieser Plan am echten Code vorfand
Dieser Plan wurde gegen den Code vom 2. Oktober 2026 geschrieben, nicht gegen die
Zeilennummern des Befunds. Die Datei ist seit dem 1. Oktober von ~11.655 auf **11.179
Zeilen** geschrumpft, und mehrere Stellen des Befunds sind durch Teil 2 überholt. Die
wichtigsten Abweichungen, damit der ausführende Agent nicht nach veralteten Zeilen
sucht:
- **Baseline ist 800 Tests, 0 Fehler** (nicht 769/773 wie im Befund und im Teil-2-Plan).
`./gradlew clean build` ist grün, Stand 2026-10-02. Verifiziert über die XML unter
`*/build/test-results/test/TEST-*.xml`: 106 Dateien, `tests=800 failures=0 errors=0
skipped=3`. Die drei übersprungenen sind Generatoren (u. a. der `derive`-Generator aus
Teil 1).
- **Scheibe B ist strukturell erledigt.** Die drei Feeds werden bei `:10133`–`:10138`
erzeugt, unmittelbar nach `composeMainWindowState = new MainWindowState(...)` bei
`:10078` — also genau in der „empfohlenen Endform" des Befunds.
`FeedLifetimeIsTiedToTheWindowTest` hält das fest. **`openComposeMainWindowIfRequested` existiert unter
diesem Namen nicht mehr**; der erzeugende Rumpf ist die Methode um `:10078`.
- **Die Zuführungs-Ziele sind bereits entkoppelt.** `updateTimelineVisuals` (`:4802`)
hat keine Wache mehr auf `timelineView` am Compose-Datenpfad; es schiebt bei
`:4839`–`:4846` in `timelineFeed`, wenn der nicht null ist, und berührt `timelineView`
nur noch im eigenen, toten Zweig (`:4819`–`:4821`).
- **Die drei Schnittlinien sind im Code markiert.** `updateTimelineVisuals`,
`updateConnectionStateIndicator` (`:5693`, Kommentar bei `:5736`: „part 3 cuts at this
line") und `initFurtherInfoAbtCallsignMSGTable` (`:3220`, Kommentar bei `:3406`: „the
binding itself still lives in this JavaFX builder - prising it out is part 3") sagen
selbst, wo Teil 3 schneidet.
- **Scheibe D ist im Compose-Code fertig.** `MainMenuBar.kt` trägt alle Aktionen, das
schreibgeschützte macOS-Verbindungsmenü und den auskommentierten Changelog-Eintrag
(`openChangelog`, als „kept so the bar can say so" dokumentiert). Für Teil 3 bleibt
nur, `initMenuBar` in der JavaFX-Datei zu löschen, nachdem der Abgleich bestätigt,
dass nichts Portierbares verloren geht.
- **Scheibe E ist halb erledigt.** Die Doppel-Zustellung in
`updateConnectionStateIndicator` ist schon auf „one write" reduziert
(`:5712`–`:5717`), der Feed-Push sitzt oberhalb der Schnittlinie. Offen ist nur das
Löschen des JavaFX-Indikators unterhalb der Linie, zusammen mit
`initConnectionStateIndicatorButton`.
**Daraus folgt der Zuschnitt dieses Plans.** Scheibe B und D brauchen keine eigene
Entflechtungsaufgabe mehr. Übrig bleiben: eine echte Entflechtung (Scheibe C, die
Bindung aus dem Builder lösen), die Löschscheiben der `init…`-Methoden, das Schneiden
der drei gemischten Methoden an ihrer Linie und zuletzt die zeilenweise Zerlegung von
`start()`.
## Warum nur Teil 3
Die Spezifikation hat sechs Etappen. Teil 1 deckte Etappe 1 und 3 ab, Teil 2 den ersten
Abschnitt von Etappe 2. Dieser Plan ist der **Rest von Etappe 2**: der durchsetzte
Aufbau in `Kst4ContestApplication` wird gelöscht, bis die Klasse auf ihre lebende Fläche
zusammengeschrumpft ist. Was danach kommt — der `UiDispatcher`-Tausch (Etappe 4), der
Lebenszyklus samt der zehn `Alert`-Dialoge (Etappe 5), die Restdateien und die Befreiung
des Builds von `org.openjfx` (Etappe 6) — ist **nicht** Teil dieses Plans. Diese Etappen
sind erst schreibbar, wenn `start()` zerlegt ist; sie vorwegzunehmen hieße raten.
## 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 Push, Merge, Tag oder Release. Die Commit-Schritte unten sind durch die Freigabe
dieses Plans gedeckt, nichts darüber hinaus.
- **Nicht** aus Exit-Code 0 auf „alle Tests grün" schließen — die XML-Ergebnisse unter
`*/build/test-results/test/TEST-*.xml` lesen (`AGENTS.md`). Das Zählskript steht in
Aufgabe 0.
- Ausgangslage: `./gradlew clean build` → exit 0, **800 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.
- `ObservableRoster` ist der Speicher, die Listen sind Spiegel. Kein Worker-Faden fasst
UI-Sammlungen an. Zustellung nur über `UiDispatcher` (`AGENTS.md`).
- Zoom, Auswahl, Fokus, Sortierung und vorbelegter Text ändern sich nicht als
Nebeneffekt (`AGENTS.md`).
- **Vor jeder Löschung** den Durchlauf laufen lassen:
`python3 docs/superpowers/notes/javafx-compose-sweep.py`.
- Die 91 MP3-Dateien in `core/src/main/resources` **bleiben** — sie gehören der lebenden
CW-Ausgabe in `PlayAudioUtils`, nicht dem toten `playCWLauncher`.
## Review Focus
Sechs Dinge, die die Spezifikation voraussetzt, die aber keine Aufgabe hier von sich aus
prüft, nach Wahrscheinlichkeit geordnet, dass sie jemanden treffen:
1. **Eine Löschung nimmt der Compose-Oberfläche still einen Anstoß.** Das ist die
Fehlerklasse, die Teil 1 gefunden hat: kein Übersetzungsfehler, kein Testfehler, nur
eine leere Tabelle oder eine eingefrorene Zeitleiste. Jede Löschscheibe bekommt
deshalb einen Anwendungsstart als Abnahme, nicht nur einen grünen Build — und bei den
drei Zuführungen zusätzlich eine konkrete Bedienhandlung (Station wählen, Verbindung
wechseln, Sked eintreffen lassen).
2. **Ein Feld überlebt die Methode, die es allein benutzte.** Der Übersetzer meldet ein
unbenutztes privates Feld nicht als Fehler. Jede Löschscheibe prüft deshalb mit einem
Grep auf die Feldnamen, ob außerhalb des gelöschten Bereichs noch eine Fundstelle
steht.
3. **Eine „tote" Methode war über einen Compose-Rückruf doch erreichbar.** Die
Compose-Fenster rufen in `Kst4ContestApplication` zurück. Das Durchlauf-Skript prüft
das transitiv bis Tiefe 3; jede Löschscheibe lässt es **vorher** laufen und bricht
ab, wenn eine zu löschende Methode als `REACHES COMPOSE` gemeldet wird.
4. **Scheibe C löst die falsche Hälfte der Doppel-Zustellung.** `refresh()` **und** ein
`Platform.runLater` schieben heute beide (`:320`/`:10381`); der Listener bei `:3410`
schiebt zusätzlich. Aufgabe 1 benennt ausdrücklich, welcher Weg bleibt, und prüft mit
einem Zähler, dass die ausgewählte Station nach dem Umbau genau einmal zugestellt
wird.
5. **Die zeilenweise Zerlegung von `start()` lässt einen lebenden Listener fallen.** In
`start()` liegen tote `timelineView`-Zeilen und lebende Auslöser dicht beieinander
(`:6927`, `:6930`, `:6945`, `:9069`). Aufgabe 7 trennt sie einzeln und hält jeden
lebenden Auslöser mit einer Begründung fest, warum er bleibt.
6. **Das Durchlauf-Skript meldet eine Methode fälschlich sauber, weil `COMPOSE_TOKENS`
unvollständig ist.** Kommt in Teil 3 ein neues Compose-Fenster oder eine neue
`replace…`-Senke dazu, ist die Liste im Skript zu pflegen. Aufgabe 0 prüft die Liste
einmal gegen die heute existierenden Compose-Senken.
---
## File Structure
| Datei | Verantwortung | Aufgabe |
|---|---|---|
| `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java` | **ändern** — Bindung entflechten, dann in Scheiben löschen | 1–7 |
| `app-desktop/src/test/kotlin/kst4contest/view/feed/SelectedStationMessagesFeedTest.kt` | **ändern** — ein Zähler-Test für die eine Zustellung (Review Focus 4) | 1 |
| `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md` | **ändern** — abgearbeitete Scheiben abhaken | 1–7 |
Keine neue Produktionsdatei. Teil 3 ist Entflechten und Löschen in **einer** Datei; die
Zuführungsklassen stehen schon.
---
## Task 0: Baseline festnageln und das Durchlauf-Skript prüfen
Keine Produktionsänderung. Diese Aufgabe stellt sicher, dass die folgenden Löschungen
auf einem belastbaren Netz stehen, und dass das Durchlauf-Skript die heutigen
Compose-Senken kennt.
**Files:**
- Read only: `docs/superpowers/notes/javafx-compose-sweep.py`,
`app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- [ ] **Step 1: Den grünen Ausgangsstand bestätigen**
Run:
```bash
cd /home/philipp/Projects/ham/kst4contest
./gradlew clean build --console=plain > /tmp/t3-base.txt 2>&1; echo "exit=$?"
python3 - <<'PY'
import glob, xml.etree.ElementTree as ET
t=f=e=s=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")); e+=int(r.get("errors")); s+=int(r.get("skipped"))
print(f"tests={t} failures={f} errors={e} skipped={s}")
PY
```
Erwartet: `exit=0`, `tests=800 failures=0 errors=0 skipped=3`. Weicht die Zahl ab, ist
der Ausgangsstand ein anderer als beim Schreiben dieses Plans — anhalten und die
Abweichung klären, bevor etwas gelöscht wird.
- [ ] **Step 2: Den Durchlauf laufen lassen und die drei lebenden Methoden sehen**
Run: `python3 docs/superpowers/notes/javafx-compose-sweep.py`
Erwartet: Genau drei `init…`-Methoden als `REACHES COMPOSE`:
`initConnectionStateIndicatorButton`, `initFurtherInfoAbtCallsignMSGTable`, `initMenuBar`.
Die Zeitleiste ist keine `init…`-Methode und erscheint nicht; sie wird in Aufgabe 7
behandelt. Meldet das Skript **mehr** als drei, ist eine neue Compose-Senke dazugekommen
und der Plan ist vor dem Löschen an ihr zu messen.
- [ ] **Step 3: `COMPOSE_TOKENS` gegen die heutigen Senken prüfen**
Run: `grep -nE "replace[A-Z]\w+|Window\.INSTANCE|composeMainWindowState" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java | grep -oE "replace[A-Z]\w+|[A-Z]\w+Window" | sort -u`
Erwartet: Jeder gefundene `replace…`-Name und jedes `…Window` steht in `COMPOSE_TOKENS`
im Skript. Fehlt einer (etwa `replaceRows`, `replaceSkeds`, `replaceCandidates`,
`StationMapWindow`), ist das Skript zu ergänzen, **bevor** es als Prüfung benutzt wird —
sonst meldet es eine Methode fälschlich sauber (Review Focus 6).
Keine Datei wird in dieser Aufgabe geändert und kein Commit erzeugt.
---
## Task 1: `selectedCallSignInfoMessageBinding` aus dem JavaFX-Builder lösen (Scheibe C)
Der Code sagt selbst, dass dies Teil 3 ist: der Kommentar bei `:3406` lautet „The
binding itself still lives in this JavaFX builder - prising it out is part 3". Danach
kann `initFurtherInfoAbtCallsignMSGTable` in Aufgabe 4 fallen, ohne die Nachrichtentabelle
der ausgewählten Station mitzunehmen.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/feed/SelectedStationMessagesFeedTest.kt`
**Interfaces:**
- Consumes: `derivedBinding(...)`, `selectedStationMessagesFeed.push(List<ChatMessage>)`,
`rosterBindings`.
- Produces: nichts Neues. `selectedCallSignInfoMessageBinding` entsteht danach außerhalb
der Tabellenkonstruktion.
**Vorgefundene Doppel-Zustellung (Review Focus 4):** Heute schieben drei Wege in die
Compose-Nachrichtentabelle — der Listener auf `selectedCallSignInfoMessageBinding.list()`
(`:3410`–`:3414`), und zusätzlich `applySelectedCallSignInfoFilter` (`:319`–`:324`) und
die Stationsauswahl (`:10380`–`:10385`), die beide `refresh()` rufen **und** danach selbst
`push(...)`. `refresh()` ändert die Spiegelliste und löst den Listener aus; der folgende
`push` stellt denselben Inhalt ein zweites Mal zu. **Welcher Weg bleibt:** der Listener.
Er ist der eine Schreibweg, der an die Datenänderung gekoppelt ist; die beiden
nachgezogenen `push`-Aufrufe sind die Doppelung. Diese Aufgabe entfernt die beiden
nachgezogenen `push`-Aufrufe **nicht** — das ist eine Verhaltensänderung über die
Entflechtung hinaus und gehört, falls erwünscht, in eine eigene, benannte Aufgabe. Sie
hält den Zustand nur mit einem Zähler-Test fest, damit eine spätere Auflösung messbar
ist.
- [ ] **Step 1: Die heutige Erzeugung und ihre drei Zuführungswege lesen**
Run:
```bash
sed -n '3395,3416p' app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
sed -n '315,326p' app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
sed -n '10374,10388p' app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
grep -n "selectedCallSignInfoMessageBinding" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: Erzeugung über `derivedBinding(...)` innerhalb von `initFurtherInfoAbtCallsignMSGTable`
(um `:3402`), der Listener, der `selectedStationMessagesFeed.push(...)` ruft, und die
Nullsetzung bei `:6376`. Jede Fundstelle notieren — die Zeilen verschieben sich mit jeder
Löschung, also hier festhalten, nicht später raten.
- [ ] **Step 2: Eine eigene Erzeugungsmethode schreiben, die nichts von der Tabelle weiß**
Eine neue private Methode anlegen, die genau das tut, was heute mitten in
`initFurtherInfoAbtCallsignMSGTable` steht — die alte Bindung freigeben, die neue über
`derivedBinding` erzeugen, den Listener anmelden —, aber ohne jeden `TableView`-Bezug:
```java
/**
* Creates the mirror that feeds the Compose "messages of the selected station" table.
*
* Lifted out of initFurtherInfoAbtCallsignMSGTable, which built the never-shown
* JavaFX table around it. The feed it drives does not depend on that table existing,
* so the binding must not either - otherwise deleting the table would silently empty
* the Compose table with a green build.
*/
private void rebuildSelectedCallSignInfoMessageBinding() {
if (selectedCallSignInfoMessageBinding != null) {
selectedCallSignInfoMessageBinding.dispose();
rosterBindings.remove(selectedCallSignInfoMessageBinding);
}
selectedCallSignInfoMessageBinding = derivedBinding(
chatcontroller::selectedCallSignInfoMessages,
chatcontroller.getLst_globalChatMessageList());
selectedCallSignInfoMessageBinding.list().addListener((javafx.collections.ListChangeListener<ChatMessage>) c -> {
if (selectedStationMessagesFeed != null) {
selectedStationMessagesFeed.push(selectedCallSignInfoMessageBinding.list());
}
});
}
```
Den herausgelösten Block in `initFurtherInfoAbtCallsignMSGTable` durch einen Aufruf von
`rebuildSelectedCallSignInfoMessageBinding()` ersetzen, **bevor**
`tbl_furtherInfoAbtCallsignMSGTable.setItems(selectedCallSignInfoMessageBinding.list())`
steht — die tote Tabelle darf die Liste weiter beziehen, solange sie existiert; das
ändert sich erst mit ihrer Löschung in Aufgabe 4.
- [ ] **Step 3: Den Aufruf dorthin hängen, wo die anderen Zuführungen erzeugt werden**
In der Methode, die `composeMainWindowState` und die Feeds erzeugt (um `:10078`–`:10138`),
nach der Feed-Erzeugung `rebuildSelectedCallSignInfoMessageBinding()` aufrufen, falls die
Bindung dort noch nicht steht. **Vorsicht:** Die Bindung wird heute auch beim Öffnen des
Info-Fensters neu erzeugt (der Builder läuft je Auswahl). Prüfen, ob der neue Aufruf eine
Doppelerzeugung verursacht; ist das der Fall, nur den Builder-Aufruf behalten und diesen
Schritt überspringen — der Zähler-Test in Schritt 5 fängt eine Doppelerzeugung.
Run: `grep -n "rebuildSelectedCallSignInfoMessageBinding\|initFurtherInfoAbtCallsignMSGTable()" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: der neue Methodenname und seine Aufrufer sind sichtbar; die Erzeugung läuft
genau einmal pro Info-Fenster-Aufbau.
- [ ] **Step 4: Übersetzen**
Run: `./gradlew :app-desktop:compileJava --console=plain 2>&1 | tail -5`
Erwartet: `BUILD SUCCESSFUL`. Ein Übersetzungsfehler hier ist am billigsten zu beheben,
bevor Tests laufen.
- [ ] **Step 5: Einen Zähler-Test ergänzen, der die eine Zustellung festhält**
In `SelectedStationMessagesFeedTest.kt` einen Test ergänzen, der mit einem zählenden
`UiDispatcher` prüft, dass ein `push` genau eine Zustellung ist — dasselbe Muster, das
`TimelineFeedTest` schon benutzt (Review Focus 4). Der bestehende Test bleibt unberührt;
der neue hält fest, dass die Zuführungsklasse selbst nicht doppelt schreibt (die
Systemdoppelung über `refresh()` + nachgezogenem `push` ist davon unberührt und bleibt
bewusst bestehen — siehe Interfaces oben).
Run:
```bash
./gradlew :app-desktop:test --tests 'kst4contest.view.feed.SelectedStationMessagesFeedTest' --console=plain > /tmp/t3-1.txt 2>&1
python3 - <<'PY'
import glob, xml.etree.ElementTree as ET
for f in glob.glob("app-desktop/build/test-results/test/TEST-*SelectedStationMessagesFeedTest.xml"):
r=ET.parse(f).getroot(); print(f"tests={r.get('tests')} failures={r.get('failures')} errors={r.get('errors')}")
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])
PY
```
Erwartet: `failures=0 errors=0`, Testzahl um eins höher als zuvor.
- [ ] **Step 6: Die ganze Suite und ein Anwendungsstart**
Run:
```bash
./gradlew clean build --console=plain > /tmp/t3-1b.txt 2>&1; echo "exit=$?"
python3 - <<'PY'
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}")
PY
```
Erwartet: `exit=0`, `failures=0`, `tests=801` (800 + 1).
Dann `./gradlew :app-desktop:run`: Anwendung starten, eine Station wählen, prüfen dass
sich die Nachrichtentabelle der ausgewählten Station füllt, einen Filter umschalten,
prüfen dass sie folgt. Beenden. **Das ist die Prüfung, die kein Test leistet** (Review
Focus 1).
- [ ] **Step 7: Den Befund abhaken und committen**
In `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md` die Scheibe C als erledigt
markieren.
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
app-desktop/src/test/kotlin/kst4contest/view/feed/SelectedStationMessagesFeedTest.kt \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Prise the selected-station message binding out of the JavaFX builder
initFurtherInfoAbtCallsignMSGTable built the never-shown JavaFX table and
created selectedCallSignInfoMessageBinding in the middle of doing so. The
Compose feed the binding drives does not depend on that table, so the
binding must not either - otherwise deleting the table in a later slice
would silently empty the Compose 'messages of the selected station' table
with a green build and no compile error, the exact failure class part 1
was written to prevent.
The binding now has its own builder, rebuildSelectedCallSignInfoMessage-
Binding, with no TableView reference. A counting-dispatcher test pins that
one push is one delivery inside the feed; the system-level double delivery
(refresh() plus a trailing push) is left as found and noted, because
resolving it changes behaviour beyond this disentanglement."
```
---
## Befund nach Task 1: der Rest-Zuschnitt ist anders als zuerst geplant
Beim Vorbereiten von Task 2 am echten Code (Stand 2026-10-02, nach den Commits bis
`03b9c1bd`) sind zwei Annahmen der ersten Planfassung widerlegt worden. Die Tasks 2–7
unten sind deshalb durch die Scheiben **S1–S6** ersetzt.
1. **Es gibt keine freistehenden Blatt-Methoden mehr.** Die vier unreferenzierten
`init…`-Methoden, die die erste Fassung in Task 2 löschen wollte
(`initShortcutTable`, `initNotifyAtCallSignTable`, `initTextSnippetsTable`,
`initWkdStnTable`), sind bereits in Teil 2 gelöscht — ein Grep über den ganzen Baum
findet sie **nullmal**. Task 2 der ersten Fassung ist gegenstandslos.
2. **Jeder verbliebene Tabellen-Builder hat genau einen Aufrufer, und der sitzt im toten
`try`-Block von `start()`** (`:6961`–`:9028`; `primaryStage.setScene(...)` und
`primaryStage.show()` sind beide auskommentiert, `:9019`/`:9028`). Methode und
Aufrufer fallen also zusammen; „Blätter zuerst, Wurzeln zuletzt" greift nicht, weil es
keine Blätter ohne Aufrufer gibt.
3. **Der lebende Compose-Pfad liegt außerhalb des toten Blocks.**
`openComposeMainWindowIfRequested()` (erzeugt `MainWindowState`, die drei Feeds und
die `composeMemberListener`/`composeChatListener`) wird **nach** dem `catch` gerufen
(um `:9055`), ebenso `openMonitorWindow`, `openUpdateWindowIfAvailable`,
`openSettingsWindow` und die Zeitleisten-Erstbefüllung
`Platform.runLater(this::updateTimelineVisuals)` (um `:9079`). Der tote Szenenaufbau
ist damit ein abgrenzbarer `try`-Block, dessen lebende Zuführungen daneben, nicht
darin, liegen.
Die reglose Reihenfolge bleibt: risikoarm zuerst, die Mitgliedertabelle mit der
Filterleiste als heikelste Scheibe mit ausdrücklicher Listener-Prüfung, der
`start()`-Rest zuletzt. Jede Scheibe wird einzeln gebaut (`clean build` + XML), lässt
vorher den Durchlauf laufen, erhält jeden lebenden Listener belegt und wird committet.
Die GUI-Abnahme (Anwendungsstart) kann der ausführende Agent nicht selbst leisten; sie
wird gesammelt und am Ende von Marc gebündelt durchgeführt.
---
## S1: Die drei Statusknöpfe schneiden und löschen (Scheibe E)
`updateConnectionStateIndicator` ist gemischt: oben der lebende Feed-Push, unten der
JavaFX-Indikator. Der Code markiert die Linie selbst („part 3 cuts at this line", heute
um `:5745`). Die risikoärmste der Rest-Scheiben, deshalb zuerst.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Durchlauf und lebende Aufrufer erheben**
Run:
```bash
python3 docs/superpowers/notes/javafx-compose-sweep.py | grep -iE "connection|sked|bandupgrade"
grep -n "updateConnectionStateIndicator\|initConnectionStateIndicatorButton\|initSkedWarnIndicatorButton\|initBandUpgradeIndicatorButton\|btnConnectionStateIndicator\|tipConnectionStateIndicator\|btnSkedWarnIndicator\|btnBandUpgradeIndicator" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: `updateConnectionStateIndicator` hat lebende Aufrufer außerhalb des toten
Aufbaus (die den Feed speisen) — diese bleiben. Die drei `init…`-Knopf-Methoden werden
nur aus `start()` gerufen (`:7116`/`:7122`/`:7145`).
- [ ] **Step 2: `updateConnectionStateIndicator` an der Linie trennen**
Alles unterhalb von „part 3 cuts at this line" (Tooltip, Button-Stil, macOS-Menütexte)
entfernen. Oben bleiben: Fadenprüfung, Normalisierung (`effectiveState`/`stateDetail`),
der eine Feed-Push, `logConnectionIndicatorTransition`. Die Normalisierung bleibt — sie
ist der Vertrag, den `ConnectionStateFeedTest` festhält. Die macOS-Menüfelder
(`menuConnectionStateMacOs`/`menuItemConnectionStateDetailMacOs`) werden nur in
`initMenuBar` eingehängt und fallen mit S2.
- [ ] **Step 3: Die drei Knopf-Builder und ihre `start()`-Aufrufer löschen**
`initConnectionStateIndicatorButton`, `initSkedWarnIndicatorButton`,
`initBandUpgradeIndicatorButton` samt Javadoc entfernen, die Aufrufzeilen in `start()`
mitnehmen, und die nur von ihnen genutzten Felder. Prüfen:
Run: `grep -n "btnConnectionStateIndicator\|tipConnectionStateIndicator\|btnSkedWarnIndicator\|btnBandUpgradeIndicator" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: nach dem Löschen keine Fundstelle außerhalb der gelöschten Bereiche. **Vorsicht
beim Verbindungsknopf:** er wird außerhalb des toten Blocks in die Statusleiste gehängt
(`!isMacOs()`), diese Zeile fällt mit.
- [ ] **Step 4: Übersetzen, Suite, Durchlauf**
Run:
```bash
./gradlew clean build --console=plain > /tmp/s1.txt 2>&1; echo "exit=$?"
python3 /tmp/count_tests.py
python3 docs/superpowers/notes/javafx-compose-sweep.py | grep -iE "connection|sked|bandupgrade"
```
Erwartet: `exit=0`, `tests=800 failures=0`, die drei Methoden erscheinen nicht mehr im
Durchlauf. GUI-Abnahme (Marc, gesammelt): Verbindung herstellen/trennen → die
Compose-Statusleiste wechselt weiter.
- [ ] **Step 5: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Cut updateConnectionStateIndicator at its seam and delete the status buttons
Above the line stays - thread check, normalisation, the single feed push,
the log. Below it goes, with initConnectionStateIndicatorButton and its two
neighbours and the fields only they touched. The Compose status bar is fed
from above the line, so its badge keeps changing. The macOS menu fields
fall with initMenuBar in S2."
```
---
## S2: `initMenuBar` löschen (Scheibe D ist im Compose-Code bereits erledigt)
`MainMenuBar.kt` trägt alle Aktionen schon — beim Schreiben dieses Plans am aktuellen
`MainMenuBar.kt` bestätigt: jede aktionstragende JavaFX-Position hat eine
Compose-Entsprechung; die drei Lücken sind ein Trenner, ein zusammengelegter
Doppeleintrag und der auskommentierte `openChangelog`. Nichts zu portieren, nur zu
löschen.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Den Abgleich am echten Code bestätigen**
Run:
```bash
grep -n "setOnAction\|menubar.getMenus()\|installSharedSystemMenuBar\|Macros" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java | sed -n '1,80p'
grep -nE "Item\(|Menu\(" app-desktop/src/main/kotlin/kst4contest/view/compose/MainMenuBar.kt
```
Erwartet: Jede `setOnAction`-tragende Position hat ein `Item(...)` in `MainMenuBar.kt`.
Eine Aktion ohne Compose-Entsprechung ist ein Fund — anhalten und in den Befund
schreiben, nicht löschen. Das tote `Macros`-Menü (gebaut, nie der Leiste hinzugefügt,
Handler auskommentiert) fällt mit, es wird nicht portiert.
- [ ] **Step 2: Durchlauf, dann löschen**
`initMenuBar`, `installSharedSystemMenuBar`, die macOS-Menüfelder und das tote
`Macros`-Menü samt dem `start()`-Aufruf `mainScreenMenuBar = initMenuBar()` entfernen.
Verwaiste Hilfsmethoden/Felder (`mainScreenMenuBar`, `flwpne_StatusBar`, soweit nur hier
benutzt, `macOsConnectionStateMenuTitle` falls `MainMenuBar.kt` eine eigene Entsprechung
hat) mitnehmen; im Zweifel bleibt die Hilfsmethode und der Fund kommt in die
Commit-Nachricht.
- [ ] **Step 3: Übersetzen, Suite, Durchlauf**
Run:
```bash
./gradlew clean build --console=plain > /tmp/s2.txt 2>&1; echo "exit=$?"
python3 /tmp/count_tests.py
python3 docs/superpowers/notes/javafx-compose-sweep.py | tail -3
```
Erwartet: `exit=0`, `tests=800 failures=0`, und `0 of N init methods reach Compose
state.` — zum ersten Mal erreicht keine `init…`-Methode mehr Compose-Zustand. GUI-Abnahme
(Marc, gesammelt): die Compose-Fenster-Öffner im Menü (Einstellungen, Monitor, Karte)
laufen weiter.
- [ ] **Step 4: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete initMenuBar; its port already lives in MainMenuBar.kt
Nothing to port: MainMenuBar.kt carries every action, the read-only macOS
connection menu and the commented-out changelog item. The three gaps are a
separator, a double entry Compose merged, and a long-commented action. With
this the sweep reports zero init methods reaching Compose - the interleaved
construction is fully separated. installSharedSystemMenuBar, the macOS menu
fields and the dead Macros menu go with it."
```
---
## S3: Die Nachrichtentabellen-Scheibe löschen
Die vier Nachrichten-/Cluster-Tabellen und ihr Tab-Container, alle nur aus dem toten
`try`-Block gerufen. Die lebenden `composeChatListener` liegen außerhalb (nach dem
`catch`) und bleiben.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Durchlauf und Aufrufer prüfen**
Run:
```bash
python3 docs/superpowers/notes/javafx-compose-sweep.py | tail -3
grep -n "initChatprivateMSGTable\|initChatGeneralMSGTable\|initChatToOtherMSGTable\|initDXClusterTable\|initBottomGlobalMessageTabPane" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: keine dieser Methoden erreicht Compose; jede hat genau einen Aufrufer im toten
`try`-Block (`:7526`/`:7673`/`:7768`/`:3607`/`:3611`). Die zugehörigen toten
Selektionslistener (`privateChatselectionModel…`, `generalChatselectionModel…`) stehen
daneben und fallen mit.
- [ ] **Step 2: Löschen, verwaiste Felder mitnehmen**
Die fünf Methoden samt Javadoc und ihre `start()`-Aufrufzeilen entfernen, dazu die toten
Selektionslistener und die nur hier benutzten Felder (`tbl_generalMessageTable`,
`tbl_chatToOther…` usw.). Prüfen:
Run: `grep -n "tbl_generalMessageTable\|tbl_dxCluster\|tbl_chatToOther\|tbl_chatprivate\|bottomGlobalMessageTabPane" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: nach dem Löschen keine Fundstelle außerhalb der gelöschten Bereiche.
- [ ] **Step 3: Übersetzen, Suite**
Run:
```bash
./gradlew clean build --console=plain > /tmp/s3.txt 2>&1; echo "exit=$?"
python3 /tmp/count_tests.py
```
Erwartet: `exit=0`, `tests=800 failures=0`. GUI-Abnahme (Marc, gesammelt): die
Chat-/Cluster-Panes im Compose-Fenster füllen sich weiter (über `composeChatListener`).
- [ ] **Step 4: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete the never-shown message and cluster tables
The private, general, to-other and DX-cluster tables and their tab pane,
each called once from the dead try block in start(). The Compose chat and
cluster panes are fed by composeChatListener, which lives after the catch
and stays. Their dead selection listeners go with them."
```
---
## S4: `initFurtherInfoAbtCallsignMSGTable` und die Kompaktsteuerung löschen
Sicher erst nach Task 1 (Scheibe C): die lebende Bindung lebt seither in
`rebuildSelectedCallSignInfoMessageBinding`, nicht im Tabellen-Builder.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Durchlauf und Aufrufer prüfen**
Run:
```bash
python3 docs/superpowers/notes/javafx-compose-sweep.py | grep -iE "furtherinfo|compact"
grep -n "initFurtherInfoAbtCallsignMSGTable\|initSelectedCallSignCompactControlsPane\|rebuildSelectedCallSignInfoMessageBinding" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: `initFurtherInfoAbtCallsignMSGTable` erreicht Compose jetzt **über**
`rebuildSelectedCallSignInfoMessageBinding`. Diese Hilfsmethode bleibt; der Tabellen-
Builder und seine Aufrufer (`:895`, `:1547`) sowie
`initSelectedCallSignCompactControlsPane` (`:1523`/`:3468`) fallen.
- [ ] **Step 2: Löschen, Bindungserzeugung sichern**
Beide Methoden samt Javadoc und ihre Aufrufer entfernen. **Kritisch:** Der Aufruf von
`rebuildSelectedCallSignInfoMessageBinding()` muss erhalten bleiben — er stand bisher im
Tabellen-Builder. Wird der Builder gelöscht, muss der Aufruf an eine lebende Stelle
wandern, die bei jeder Stationsauswahl läuft (`generateFurtherInfoAbtSelectedCallsignBP`
oder deren Aufrufer). Vor dem Löschen prüfen, wo `rebuild…` dann gerufen wird:
Run: `grep -n "rebuildSelectedCallSignInfoMessageBinding\|generateFurtherInfoAbtSelectedCallsignBP" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: `rebuild…` wird weiterhin bei jeder Auswahl gerufen;
`selectedCallSignInfoMessageBinding` bleibt lebend.
- [ ] **Step 3: Übersetzen, Suite**
Run:
```bash
./gradlew clean build --console=plain > /tmp/s4.txt 2>&1; echo "exit=$?"
python3 /tmp/count_tests.py
```
Erwartet: `exit=0`, `tests=800 failures=0`. GUI-Abnahme (Marc, gesammelt): Station
wählen → die Nachrichtentabelle der ausgewählten Station füllt sich weiter.
- [ ] **Step 4: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete the selected-callsign info table and its compact controls
Safe because S/Scheibe C moved the binding into rebuildSelectedCallSign-
InfoMessageBinding first. The Compose messages-of-the-selected-station
table is fed by that binding, not by this JavaFX table, so the table goes
while the feed stays. Verified by selecting a station after the deletion."
```
---
## S5: Die Mitgliedertabelle, das Kontextmenü und die Filterleiste löschen
Die größte und heikelste Scheibe: `initChatMemberTable` (die größte Methode),
`initChatMemberTableContextMenu` und die Filter-UI im toten `try`-Block. Hier liegen
lebende und tote Listener dicht beieinander.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Jeden Listener im Filterbereich tot/lebend einordnen**
Dies ist der eigentliche Prüfschritt der Scheibe und läuft **vor** jeder Löschung.
Run:
```bash
python3 docs/superpowers/notes/javafx-compose-sweep.py | grep -iE "chatMember"
grep -n "getLst_chatMemberListFilterPredicates\|getStationFilter\|chatMemberTableFilter\|tglBtnQRBEnable\|tglGrpQTF\|selectionModelChatMember\|tbl_chatMember\b\|chatMemberContextMenu\|chatMessageContextMenu" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Für jeden Treffer entscheiden: speist er die **Compose**-Stationsliste (lebend, muss
bleiben oder umgehängt werden) oder nur die tote `TableView` (fällt)? Der Lebendbefund
legt nahe, dass die Compose-Filterung über `composeMainWindowState.getStationFilter()`
(`:9440` ff.) läuft und **nicht** über diese JavaFX-Filter-Listener — das ist hier zu
**bestätigen**, nicht anzunehmen. Jeder lebende Listener wird mit Fundstelle im Befund
notiert, bevor etwas fällt.
- [ ] **Step 2: Löschen, ggf. in zwei Commits (Kontextmenü, dann Tabelle+Filter)**
`initChatMemberTable`, `initChatMemberTableContextMenu`, die Filter-UI und die toten
Selektions-/Filter-Listener samt `start()`-Aufrufern entfernen. Das Feld
`chatMemberListBinding` und seine Erzeugung mitnehmen, sobald sein letzter Leser
(`setItems` der toten Tabelle) weg ist:
Run: `grep -n "chatMemberListBinding" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: nach dem Löschen keine Fundstelle außerhalb der gelöschten Bereiche. Steht ein
anderer Leser da, bleibt das Feld.
- [ ] **Step 3: Übersetzen, Suite**
Run:
```bash
./gradlew clean build --console=plain > /tmp/s5.txt 2>&1; echo "exit=$?"
python3 /tmp/count_tests.py
```
Erwartet: `exit=0`, `tests=800 failures=0`. GUI-Abnahme (Marc, gesammelt, **die
wichtigste**): die Compose-Stationsliste füllt sich, Filtern (QRB/QTF/Text) wirkt
weiter, Auswahl/Sortierung/`/cq`-Vorbelegung unverändert.
- [ ] **Step 4: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete the never-shown member table, its context menu and the filter bar
The roots of the window that is built every start and shown never. The
Compose station list is fed by composeMemberListener and filtered through
composeMainWindowState.getStationFilter(), both outside the dead block, so
they stay; the JavaFX table, its context menu, the filter listeners and
chatMemberListBinding - whose only reader was the dead table's setItems -
go together. Each live listener was classified before deletion; the finding
records them."
```
---
## S6: Den Rest von `start()` zerlegen
Der letzte Schnitt: der tote `timelineView`, die `scn_ChatwindowMainScene` samt Listenern
und `registerThemedScene`/`themedScenes`, der leer gewordene `try/catch`-Block, und die
tote CW-Ausgabe. Die drei lebenden Zeitleisten-Listener bleiben, von ihren
`timelineView`-Berührungen befreit.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
**Vorgefundene Verzahnung** (vor der Arbeit neu greppen):
| Stelle | Was | tot/lebend |
|---|---|---|
| `~:6878`–`:6885` | `timelineView = new TimelineView()`, Styling, Einhängen | **tot** |
| `~:6890`, `:6899` | `timelineView` Width/Height-Listener → `updateTimelineVisuals()` | **tot** |
| `~:6927` | `getActiveSkeds().addListener(... updateTimelineVisuals())` | **lebend** |
| `~:6930` | `getScoreService().uiPulse().addListener(... updateTimelineVisuals())` | **lebend** |
| `~:6941`, `:6942` | `timelineView.setCurrentAntennaAzimuth(...)`, `setBeamWidthDeg(...)` | **tot** |
| `~:6945`–`:6948` | `getActualQTF().addListener(...)`: `timelineView.setCurrentAntennaAzimuth` **und** `updateTimelineVisuals()` | **lebend, gemischt** |
| `~:9079` | `Platform.runLater(this::updateTimelineVisuals)` | **lebend** (Erstbefüllung) |
| `~:9849` | `return timelineView;` (Getter) | mit `timelineView` zu prüfen |
- [ ] **Step 1: Die Verzahnung am echten Code neu erheben**
Run:
```bash
grep -n "timelineView\|updateTimelineVisuals\|getActiveSkeds().addListener\|uiPulse().addListener\|getActualQTF().addListener\|scn_ChatwindowMainScene\|registerThemedScene\|themedScenes\|playCWLauncher\|javafx.scene.media" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Jede Zeile einem Topf zuordnen — tot (fällt) oder lebend (bleibt, oder der lebende Teil
einer gemischten Zeile bleibt).
- [ ] **Step 2: Die lebenden Auslöser vom toten Knoten trennen**
- `getActualQTF().addListener(...)` zu einem reinen `updateTimelineVisuals()`-Auslöser
machen, den `timelineView.setCurrentAntennaAzimuth`-Aufruf entfernen.
`updateTimelineVisuals` liest den Azimut ohnehin frisch aus den Preferences.
- `getActiveSkeds()`- und `uiPulse()`-Listener bleiben unverändert.
- Die Erstbefüllung `Platform.runLater(this::updateTimelineVisuals)` bleibt.
- [ ] **Step 3: Den toten `timelineView`, die tote Szene und den leeren Block löschen**
- `timelineView`-Erzeugung, Größen-Listener, Setter, Getter, Feld (`:213`) und den toten
Zweig in `updateTimelineVisuals` entfernen; der Feed-Push bleibt.
- `scn_ChatwindowMainScene` samt Größen-/Key-Listenern, `registerThemedScene` und
`themedScenes` entfernen, soweit der Grep keinen lebenden Nutzer zeigt.
- Den dann leeren/toten `try { … } catch`-Block in `start()` auflösen, ohne die lebenden
Aufrufe danach (`openMonitorWindow`, `openComposeMainWindowIfRequested`,
`openUpdateWindowIfAvailable`, `openSettingsWindow`, Erstbefüllung) zu berühren.
- Die tote CW-Ausgabe (`playCWLauncher`, `musicList`, `mediaPlayer`) und den Import
`javafx.scene.media` nur löschen, wenn der Grep sie als unerreichbar belegt.
- **`TimelineView.java` selbst bleibt** (Etappe 6). Diese Scheibe entfernt nur seine
Nutzung.
- [ ] **Step 4: Übersetzen, Suite**
Run:
```bash
./gradlew clean build --console=plain > /tmp/s6.txt 2>&1; echo "exit=$?"
python3 /tmp/count_tests.py
```
Erwartet: `exit=0`, `tests=800 failures=0`. GUI-Abnahme (Marc, gesammelt): die
Compose-Zeitleiste über dem Eingabefeld reagiert weiter auf eintreffende Skeds und
Rotordrehung.
- [ ] **Step 5: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Separate start(): drop the dead timeline node and scene, keep the triggers
The dead TimelineView node, the never-attached scn_ChatwindowMainScene with
its size and key listeners, registerThemedScene/themedScenes and the now-
empty try block are gone. The three live timeline triggers stay - the sked
listener, the uiPulse listener and the QTF listener, the last cut down to
the updateTimelineVisuals call it shares with a removed timelineView setter.
The dead CW launcher and the javafx.scene.media import go where the grep
proves nothing live reaches them. TimelineView.java itself stays for stage 6.
Verified by watching the Compose timeline update on an incoming sked and a
rotor turn - the signal a deleted live trigger would have silenced."
```
---
## Prüfung
Das Netz dieser Etappe:
- **800 Tests, 0 Fehler** als Ausgangsstand, nach jeder Scheibe neu gezählt über die XML
— nicht aus Exit-Code 0 geschlossen.
- Das **Durchlauf-Skript** vor jeder Löschung; nach S2 meldet es `0 of N`.
- Eine **GUI-Abnahme** je Scheibe, von Marc gebündelt durchgeführt (der ausführende Agent
kann die GUI nicht bedienen): Stationsliste, Nachrichtentabelle bei Auswahl,
Verbindungsbadge, Zeitleiste auf Sked/Rotor, Menü-Fensteröffner, Design-/Profilwechsel.
- Die bestehenden Feed-Tests (`TimelineFeedTest`, `SelectedStationMessagesFeedTest`,
`ConnectionStateFeedTest`, `FeedIsTheOnlyWriterTest`,
`FeedLifetimeIsTiedToTheWindowTest`).
## Was unverändert bleibt
- Die Fachlogik in `core` und sämtliche Protokollformate.
- Die **Schnittstelle** `UiDispatcher` und ihre Umsetzung `JavaFxUiDispatcher` — der
Tausch ist Etappe 4.
- Die Invarianten aus `AGENTS.md`: `ObservableRoster` ist der Speicher, die Listen sind
Spiegel; kein Worker-Faden fasst UI-Sammlungen an; Zoom, Auswahl, Fokus, Sortierung und
vorbelegter Text ändern sich nicht als Nebeneffekt.
- Die 91 MP3-Dateien und die lebende CW-Ausgabe in `PlayAudioUtils`.
- `TimelineView.java`, `PathProfileChart.java`, `StationMapView.java` und die übrigen
JavaFX-Dateien als Dateien — nur ihre Nutzung in `Kst4ContestApplication` fällt.
- Die vorgefundene System-Doppelzustellung der Nachrichtentabelle — in Task 1
festgehalten, nicht aufgelöst.
## Risiken
**Eine Löschung nimmt der Compose-Oberfläche still einen Anstoß.** Die Fehlerklasse, der
dieser Plan gilt. Jede Scheibe hat deshalb eine GUI-Abnahme, nicht nur einen grünen
Build. S5 (Mitgliedertabelle + Filterleiste) ist die gefährlichste, weil dort lebende und
tote Listener am dichtesten liegen; ihr Step 1 ordnet jeden Listener vor dem Schnitt ein.
**Die Zeilennummern altern mit jeder Scheibe.** Jede Scheibe erhebt ihre Stellen vor der
Arbeit neu per Grep; keine Löschung stützt sich auf eine notierte Zeile, nur auf den
Namen.
**Die GUI-Abnahme ist ausgelagert.** Der ausführende Agent kann die Anwendung nicht
bedienen; die Abnahme je Scheibe liegt bei Marc. Bis dahin ist ein grüner Build ein
notwendiger, aber kein hinreichender Nachweis.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Den Durchlauf laufen lassen**
Run: `python3 docs/superpowers/notes/javafx-compose-sweep.py`
Erwartet: unverändert drei `REACHES COMPOSE`. Keine der hier zu löschenden Methoden darf
darunter sein.
- [ ] **Step 2: Für jede verdächtige `init…`-Methode die Aufrufer am echten Code zählen**
Run:
```bash
for m in initDXClusterTable initChatToOtherMSGTable initBottomGlobalMessageTabPane \
initChatGeneralMSGTable initChatprivateMSGTable initChatMemberTable \
initChatMemberTableContextMenu initSelectedCallSignCompactControlsPane \
initTopPriorityListPane initSkedWarnIndicatorButton initBandUpgradeIndicatorButton; do
echo "--- $m"
grep -rn "\b$m\b" --include='*.java' --include='*.kt' app-desktop/src core/src
done
grep -cE "getDeclaredMethod|getMethod\(|Class\.forName|FXMLLoader" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: Für jede Methode genau die Deklaration und ihre echten Aufrufer; die letzte
Zeile ist `0` (keine reflektive oder FXML-Hintertür). **Diese Scheibe löscht nur
Methoden, deren einziger Aufrufer selbst in derselben Scheibe gelöscht wird oder die gar
keinen Aufrufer haben.** Welche das sind, entscheidet die Ausgabe — nicht der Befund,
dessen Zeilen veraltet sind. Methoden, die noch von lebendem Compose-Code erreicht
werden, bleiben für spätere Scheiben.
- [ ] **Step 3: Die Blatt-Scheibe löschen**
Die in Schritt 2 als blattartig bestätigten Methoden vollständig entfernen, je mit
Javadoc-Block. Die Konstruktionskette von `scn_ChatwindowMainScene` baut diese Tabellen
ineinander; eine Methode, die eine noch lebende Methode aufruft, bleibt. Blätter zuerst:
die Tabellen, die niemand mehr baut, bevor der Rahmen fällt, der sie baute.
- [ ] **Step 4: Verwaiste Felder mitnehmen**
Run:
```bash
grep -nE "tbl_dxClusterTable|tbl_chatToOtherMSGTable|tbl_chatGeneralMSGTable|tbl_chatprivateMSGTable|tbl_chatMemberTable" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: Jedes Feld, dessen einzige Nutzer gelöscht wurden, erscheint nur noch in seiner
Deklaration — dann mit entfernen. Bleibt eine Fundstelle außerhalb der gelöschten
Bereiche, gehört das Feld jemandem und bleibt (Review Focus 2).
- [ ] **Step 5: Übersetzen, Suite, Anwendungsstart**
Run:
```bash
python3 docs/superpowers/notes/javafx-compose-sweep.py | tail -3
./gradlew clean build --console=plain > /tmp/t3-2.txt 2>&1; echo "exit=$?"
python3 - <<'PY'
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}")
PY
```
Erwartet: `exit=0`, `failures=0`, Testzahl **unverändert** (die gelöschten Methoden hatte
kein Test berührt). Dann `./gradlew :app-desktop:run`: Anwendung startet, Stationsliste
füllt sich, keine `NullPointerException`/`NoSuchMethodError` auf der Konsole. Beenden.
- [ ] **Step 6: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete the never-shown station and message table builders
The slices from the live-code finding, confirmed against today's tree:
each deleted builder's only reachable caller was itself deleted in the
same slice, and the sweep script reports none of them reaching Compose
state. getDeclaredMethod, getMethod(, Class.forName and FXMLLoader are
still zero in the file, so there is no reflective back door either.
The test count did not move: nothing ever exercised this construction.
That is the premise the whole removal rests on, re-proven on a tree that
is now several slices smaller than when it was first stated."
```
---
## Task 3: Die Nachrichten- und Kompaktsteuerungs-Scheibe löschen
Nach Aufgabe 1 ist `initFurtherInfoAbtCallsignMSGTable` von seiner lebenden Zuführung
getrennt und kann mit seiner Nachbarschaft fallen.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Durchlauf und Aufrufer prüfen**
Run:
```bash
python3 docs/superpowers/notes/javafx-compose-sweep.py | grep -E "FurtherInfo|SelectedCallSignCompact"
grep -n "initFurtherInfoAbtCallsignMSGTable\|initSelectedCallSignCompactControlsPane\|tbl_furtherInfoAbtCallsignMSGTable" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: `initFurtherInfoAbtCallsignMSGTable` wird vom Durchlauf **nicht mehr** als
`REACHES COMPOSE` gemeldet (Aufgabe 1 hat die Compose-Senke entfernt). Steht es noch dort,
ist Aufgabe 1 unvollständig — anhalten. Die verbliebenen Aufrufer müssen selbst in dieser
oder einer früheren Scheibe gelöscht sein.
- [ ] **Step 2: Löschen, Felder mitnehmen, Binding-Erzeugung sichern**
Die beiden Methoden samt Javadoc entfernen. **Kritisch:** `selectedCallSignInfoMessageBinding`
darf durch das Löschen von `initFurtherInfoAbtCallsignMSGTable` **nicht** verschwinden —
seine Erzeugung lebt seit Aufgabe 1 in `rebuildSelectedCallSignInfoMessageBinding()`.
Prüfen, dass dieser Aufruf erhalten bleibt:
Run: `grep -n "rebuildSelectedCallSignInfoMessageBinding\|selectedCallSignInfoMessageBinding" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: die Bindung wird weiter in `rebuildSelectedCallSignInfoMessageBinding` erzeugt
und vom Feed-Listener benutzt; der `setItems`-Aufruf auf die gelöschte Tabelle ist weg.
- [ ] **Step 3: Übersetzen, Suite, Anwendungsstart**
Run: wie Aufgabe 2, Schritt 5.
Erwartet: `exit=0`, `failures=0`, Testzahl unverändert gegenüber Aufgabe 1 (801). Beim
Anwendungsstart **erneut** eine Station wählen und prüfen, dass die Nachrichtentabelle
sich füllt — das ist die Scheibe, bei der ein Fehler in Aufgabe 1 sichtbar würde.
- [ ] **Step 4: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete the selected-callsign info table and its compact controls
Safe only because task 1 moved selectedCallSignInfoMessageBinding into its
own builder first. The Compose 'messages of the selected station' table is
fed by that binding, not by this JavaFX table, so the table goes while the
feed stays. Verified by selecting a station after the deletion and watching
the Compose table fill."
```
---
## Task 4: Die großen Tabellen-Scheiben löschen (Mitglieder-, Chat-, Kontextmenü)
Die Wurzeln des nie gezeigten Fensters: `initChatMemberTable` (die größte Methode) und
ihr Kontextmenü, die privaten und allgemeinen Chat-Tabellen. Sie fallen, nachdem die
Blätter weg sind.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Durchlauf und Aufrufer prüfen**
Run:
```bash
python3 docs/superpowers/notes/javafx-compose-sweep.py | tail -3
grep -n "initChatMemberTable\b\|initChatMemberTableContextMenu\|initChatprivateMSGTable\|initChatGeneralMSGTable" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
grep -n "chatMemberListBinding" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: keine dieser Methoden erreicht Compose. `chatMemberListBinding` (das Feld, das
der Befund als „mit der toten Tabelle löschbar" ausweist) hat als einzigen Leser den
`setItems`-Aufruf der toten Mitgliedertabelle — der mit ihr fällt. Steht ein anderer
Leser da, bleibt das Feld.
- [ ] **Step 2: Löschen, in sinnvoller Teilung**
`initChatMemberTable` ist groß genug, um die Scheibe bei Bedarf in zwei Commits zu teilen
(Kontextmenü zuerst, dann die Tabelle). Jede Teillöschung einzeln bauen. Das Feld
`chatMemberListBinding` und seine Erzeugung mitnehmen, sobald sein letzter Leser weg ist.
- [ ] **Step 3: Übersetzen, Suite, Anwendungsstart**
Run: wie Aufgabe 2, Schritt 5.
Erwartet: `exit=0`, `failures=0`, Testzahl unverändert. Beim Anwendungsstart die
Stationsliste und die Chat-Tabellen im Compose-Fenster prüfen — sie werden von den
Compose-Listenern (`composeMemberListener`, `composeChatListener`) gespeist, nicht von
diesen gelöschten JavaFX-Tabellen; sie müssen sich weiter füllen.
- [ ] **Step 4: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete the never-shown member and chat tables
The roots of the window that is built every start and shown never. The
Compose station list and chat panes are fed by composeMemberListener and
composeChatListener, which stay; these JavaFX tables and chatMemberList-
Binding, whose only reader was the dead table's setItems, go together.
Verified by watching the Compose lists fill after the deletion."
```
---
## Task 5: Die drei Statusknöpfe schneiden und löschen (Scheibe E inbegriffen)
`initConnectionStateIndicatorButton` ist gemischt: oben der lebende Feed-Push, unten der
JavaFX-Indikator. Der Code markiert die Linie bei `:5736` („part 3 cuts at this line").
`initSkedWarnIndicatorButton` und `initBandUpgradeIndicatorButton` sind die beiden
Nachbarn.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Die Schnittlinie und die lebenden Aufrufer lesen**
Run:
```bash
sed -n '5693,5760p' app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
grep -n "updateConnectionStateIndicator\|initConnectionStateIndicatorButton\|btnConnectionStateIndicator\|tipConnectionStateIndicator\|menuConnectionStateMacOs\|menuItemConnectionStateDetailMacOs" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: `updateConnectionStateIndicator` wird außerhalb des toten Aufbaus gerufen (die
lebenden Aufrufer, die den Feed speisen). Diese Aufrufer **bleiben**; nur der
JavaFX-Rumpf unterhalb der Schnittlinie fällt. Die macOS-Felder `menuConnectionStateMacOs`
/ `menuItemConnectionStateDetailMacOs` werden nur in `initMenuBar` eingehängt und fallen
mit Aufgabe 6.
- [ ] **Step 2: `updateConnectionStateIndicator` an der Linie trennen**
Alles unterhalb von „part 3 cuts at this line" (`:5736` ff.: Tooltip, Button-Stil,
macOS-Menütexte) entfernen. Oben bleiben: die Fadenprüfung, die Normalisierung
(`effectiveState`/`stateDetail`), der eine Feed-Push und `logConnectionIndicatorTransition`.
**Die Normalisierung bleibt**, auch wenn sie damit nur noch den Feed-Push und das Log
speist — sie ist der Vertrag, den `ConnectionStateFeedTest` festhält.
Prüfen, ob die Methode nach dem Schnitt noch `tipConnectionStateIndicator` oder
`btnConnectionStateIndicator` berührt; wenn nicht, können diese Felder mit
`initConnectionStateIndicatorButton` fallen.
- [ ] **Step 3: Die drei Knopf-Builder löschen**
`initConnectionStateIndicatorButton`, `initSkedWarnIndicatorButton`,
`initBandUpgradeIndicatorButton` samt Javadoc entfernen, und die nur von ihnen benutzten
Felder (`btnConnectionStateIndicator`, `tipConnectionStateIndicator` und die beiden
Nachbarknöpfe) mitnehmen. Prüfen:
Run: `grep -n "btnConnectionStateIndicator\|tipConnectionStateIndicator\|btnSkedWarn\|btnBandUpgrade" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: nach dem Löschen keine Fundstelle mehr außerhalb der gelöschten Bereiche.
- [ ] **Step 4: Übersetzen, Suite, Anwendungsstart**
Run: wie Aufgabe 2, Schritt 5.
Erwartet: `exit=0`, `failures=0`, Testzahl unverändert. Beim Anwendungsstart die
Verbindung herstellen/trennen und prüfen, dass die Verbindungsanzeige in der
**Compose**-Statusleiste weiter wechselt — der Feed-Push oberhalb der Linie speist sie,
der gelöschte JavaFX-Indikator war nie sichtbar (Review Focus 1).
- [ ] **Step 5: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Cut updateConnectionStateIndicator at its seam and delete the buttons
The method mixed a live feed push with the JavaFX indicator of the window
that is never shown; the code already marked where part 3 cuts. Above the
line stays - thread check, normalisation, the single feed push, the log.
Below it goes, with initConnectionStateIndicatorButton and its two
neighbours and the fields only they touched.
The Compose status bar's connection badge is fed from above the line, so
it keeps changing; verified by connecting and disconnecting after the cut.
The macOS menu fields fall with initMenuBar in the next task."
```
---
## Task 6: `initMenuBar` löschen (Scheibe D ist im Compose-Code bereits erledigt)
`MainMenuBar.kt` trägt alle Aktionen schon — der Abgleich ist im Befund gemacht und beim
Schreiben dieses Plans am aktuellen `MainMenuBar.kt` erneut bestätigt: jede
aktionstragende JavaFX-Position hat eine Compose-Entsprechung; die drei Lücken sind ein
Trenner, ein in Compose zusammengelegter Doppeleintrag und ein seit Längerem
auskommentierter Eintrag (`openChangelog`, in `MainMenuBar.kt` ausdrücklich „kept so the
bar can say so"). Es ist also nichts zu portieren, nur zu löschen.
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
- [ ] **Step 1: Den Abgleich am echten Code bestätigen**
Run:
```bash
grep -n "initMenuBar\|installSharedSystemMenuBar\|menubar.getMenus()\|setOnAction" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java | sed -n '1,60p'
grep -nE "Item\(|Menu\(" app-desktop/src/main/kotlin/kst4contest/view/compose/MainMenuBar.kt
```
Erwartet: Jede `setOnAction`-tragende JavaFX-Position hat ein `Item(...)` in
`MainMenuBar.kt`. Taucht eine Aktion in JavaFX auf, die in Compose fehlt, ist das ein
Fund — anhalten und ihn in den Befund schreiben, **nicht** löschen. Das tote `Macros`-Menü
(gebaut, nie der Leiste hinzugefügt, Handler auskommentiert) fällt mit, es wird nicht
portiert.
- [ ] **Step 2: Den Durchlauf prüfen und löschen**
Run: `python3 docs/superpowers/notes/javafx-compose-sweep.py | grep -i menubar`
Erwartet: `initMenuBar` ist zu diesem Zeitpunkt die einzige verbliebene `REACHES
COMPOSE`-Meldung — und das zu Recht, weil seine Einträge Compose-Fenster öffnen. Das
Löschen entfernt diese Aufrufe mit; die Compose-Fenster werden weiter über
`ComposeMenuActions` / `MainMenuBar.kt` geöffnet, nicht über diese JavaFX-Leiste.
`initMenuBar`, `installSharedSystemMenuBar` und die macOS-Menüfelder samt dem toten
`Macros`-Menü entfernen.
- [ ] **Step 3: Verwaiste Felder und Hilfsmethoden mitnehmen**
Run: `grep -nE "menubar\b|macOsConnectionStateMenuTitle|menuConnectionStateMacOs|menuItemConnectionStateDetailMacOs|fileMenu|optionsMenu|windowMenu|helpMenu" app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
Erwartet: Felder und Hilfsmethoden, deren einzige Nutzer in `initMenuBar` standen,
erscheinen nur noch in ihrer Deklaration — mit entfernen. **`macOsConnectionStateMenuTitle`
prüfen:** falls `MainMenuBar.kt` eine eigene Entsprechung hat
(`ConnectionIndicator.macOsMenuTitle`), ist die Java-Fassung verwaist und fällt; falls nicht, bleibt sie, bis Etappe 5
den macOS-Pfad klärt. Im Zweifel bleibt sie und der Fund kommt in die Commit-Nachricht.
- [ ] **Step 4: Übersetzen, Suite, Anwendungsstart**
Run: wie Aufgabe 2, Schritt 5.
Erwartet: `exit=0`, `failures=0`, Testzahl unverändert. Beim Anwendungsstart jeden
Compose-Fenster-Öffner aus dem Menü prüfen (Einstellungen, Monitor, Karte) — sie laufen
über `MainMenuBar.kt`, nicht über die gelöschte Leiste.
- [ ] **Step 5: Durchlauf-Erfolg festhalten**
Run: `python3 docs/superpowers/notes/javafx-compose-sweep.py | tail -3`
Erwartet: `0 of N init methods reach Compose state.` — zum ersten Mal erreicht **keine**
`init…`-Methode mehr Compose-Zustand. Das ist die messbare Abnahme, dass der durchsetzte
Aufbau getrennt ist.
- [ ] **Step 6: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Delete initMenuBar; its port already lives in MainMenuBar.kt
Nothing to port: MainMenuBar.kt carries every action the JavaFX bar did,
the read-only macOS connection menu, and even the commented-out changelog
item, kept there so the bar can say so. The three gaps are a separator, a
double entry Compose merged, and an action that has been commented out for
some time.
With this the sweep reports zero of the init methods reaching Compose
state - the first time the interleaved construction is fully separated.
installSharedSystemMenuBar, the macOS menu fields and the dead Macros menu
(built, never added, handlers commented out) go with it."
```
---
## Task 7: `start()` Zeile für Zeile zerlegen
Der letzte und heikelste Schnitt dieser Etappe. In `start()` (`:6855` ff.) liegen tote
`timelineView`-Zeilen und **lebende** Auslöser dicht beieinander. `start()` wird nicht am
Stück beschnitten, sondern Zeile für Zeile getrennt (Review Focus 5).
**Files:**
- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java`
- Modify: `docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md`
**Die vorgefundene Verzahnung** (Stand 2026-10-02, Zeilen verschieben sich mit den
früheren Scheiben — vor der Arbeit neu greppen):
| Stelle | Was | tot/lebend |
|---|---|---|
| `:6878`–`:6885` | `timelineView = new TimelineView()`, Styling, Einhängen in `pnl_inputAndSendButtons` | **tot** — der Knoten wird nie gezeigt |
| `:6890`, `:6899` | `timelineView` Width/Height-Listener → `updateTimelineVisuals()` | **tot** — feuern nur, wenn der tote Knoten Größe bekommt |
| `:6927` | `chatcontroller.getActiveSkeds().addListener(... updateTimelineVisuals())` | **lebend** — speist die Compose-Zeitleiste |
| `:6930` | `chatcontroller.getScoreService().uiPulse().addListener(... updateTimelineVisuals())` | **lebend** |
| `:6941`, `:6942` | `timelineView.setCurrentAntennaAzimuth(...)`, `setBeamWidthDeg(...)` | **tot** — füttern nur den toten Knoten |
| `:6945`–`:6948` | `getActualQTF().addListener(...)`: ruft `timelineView.setCurrentAntennaAzimuth` **und** `updateTimelineVisuals()` | **lebend, gemischt** — der `updateTimelineVisuals`-Teil bleibt, der `timelineView`-Teil fällt |
| `:7823` | `timelineView.setOnCandidateClicked(...)` | **tot** |
| `:9069` | `Platform.runLater(this::updateTimelineVisuals)` | **lebend** — die Erstbefüllung |
| `:9849` | `return timelineView;` (Getter) | mit `timelineView` zu prüfen |
- [ ] **Step 1: Die Verzahnung am echten Code neu erheben**
Run:
```bash
grep -n "timelineView\|updateTimelineVisuals\|getActiveSkeds().addListener\|uiPulse().addListener\|getActualQTF().addListener" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: dieselbe Verzahnung wie in der Tabelle oben, mit aktuellen Zeilen. Jede Zeile
genau einem Topf zuordnen — tot (fällt) oder lebend (bleibt, oder der lebende Teil einer
gemischten Zeile bleibt).
- [ ] **Step 2: Die lebenden Auslöser vom toten Knoten trennen**
- `:6945`–`:6948` zu einem reinen `updateTimelineVisuals()`-Auslöser machen, den
`timelineView.setCurrentAntennaAzimuth`-Aufruf entfernen. `updateTimelineVisuals` liest
den Azimut ohnehin frisch aus den Preferences (`:4843`), der tote Knoten brauchte ihn
nicht.
- `:6927` und `:6930` bleiben unverändert — sie rufen schon nur `updateTimelineVisuals()`.
- `:9069` bleibt — die Erstbefüllung.
- [ ] **Step 3: Den toten `timelineView` und seinen eigenen Zweig löschen**
- Die Erzeugung (`:6878`–`:6885`), die beiden Größen-Listener, `setCurrentAntennaAzimuth`
/`setBeamWidthDeg`, `setOnCandidateClicked` und den Getter entfernen.
- In `updateTimelineVisuals` den toten Zweig `if (timelineView != null) { timelineView.updateVisuals(...) }`
(`:4819`–`:4821`) entfernen; der Feed-Push darunter bleibt.
- Das Feld `private TimelineView timelineView;` (`:213`) fällt zuletzt.
- **`TimelineView.java` selbst bleibt** — es ist eine eigene Datei und gehört in Etappe 6
(Restdateien), nicht hierher. Diese Aufgabe entfernt nur seine Nutzung in
`Kst4ContestApplication`.
- [ ] **Step 4: Die tote CW-Ausgabe und die tote Szene mitnehmen, soweit erreichbar**
Der Befund nennt für den Rest von `start()` auch die tote CW-Ausgabe (`playCWLauncher`,
`musicList`, `mediaPlayer`, Import `javafx.scene.media`) und die nie gehängte
`scn_ChatwindowMainScene` samt Listenern und `themedScenes`. Am echten Code prüfen, was
davon nach den Scheiben 2–6 noch steht und ohne lebende Nebenwirkung fällt:
Run:
```bash
grep -n "playCWLauncher\|musicList\|mediaPlayer\|javafx.scene.media\|scn_ChatwindowMainScene\|themedScenes\|registerThemedScene" \
app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java
```
Erwartet: `playCWLauncher` ohne Aufrufer (der Befund nennt ihn tot), `registerThemedScene`
/`themedScenes` nach dem Wegfall des Kandidatenfensters (Teil 2) und der toten Hauptszene
ohne lebenden Nutzer. Jede dieser Gruppen nur löschen, wenn der Grep bestätigt, dass kein
lebender Pfad sie erreicht; sonst in den Befund schreiben und stehen lassen. Der Import
`javafx.scene.media` fällt mit der toten CW-Ausgabe.
- [ ] **Step 5: Übersetzen, Suite, Anwendungsstart — mit Sked-Prüfung**
Run: wie Aufgabe 2, Schritt 5.
Erwartet: `exit=0`, `failures=0`, Testzahl unverändert. Beim Anwendungsstart die
**Compose-Zeitleiste** über dem Eingabefeld prüfen: ein eintreffender Sked und eine
Rotordrehung müssen sie weiter aktualisieren (Marker, Antennenrichtung). Bleibt sie leer
oder eingefroren, ist ein lebender Auslöser mitgelöscht worden — die Fehlerklasse, die
dieser Plan verhindern soll (Review Focus 5).
- [ ] **Step 6: Abhaken und committen**
```bash
git add app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java \
docs/superpowers/notes/2026-10-01-javafx-lebendbefund.md
git commit -m "Separate start() line by line: drop the dead timeline node, keep its triggers
In start() the dead TimelineView node and the live triggers that drive the
Compose timeline sat next to each other. The node, its size listeners, its
azimuth setters, its click handler and its getter are gone; the three live
triggers - the sked listener, the uiPulse listener and the QTF listener -
stay, with the QTF one cut down to the updateTimelineVisuals call it shares
with a now-removed timelineView setter. updateTimelineVisuals reads the
azimuth fresh from preferences, so the setter was never load-bearing.
The dead CW launcher and the never-attached main scene go where the grep
proves no live path reaches them, taking the javafx.scene.media import with
them. TimelineView.java itself stays for stage 6.
Verified by watching the Compose timeline still update on an incoming sked
and a rotor turn - the exact signal a deleted live trigger would have
silenced with a green build."
```
---
## Prüfung
Das Netz dieser Etappe:
- **800 Tests, 0 Fehler** als Ausgangsstand (Aufgabe 0), nach jeder Scheibe neu gezählt
über die XML — nicht aus Exit-Code 0 geschlossen.
- Das **Durchlauf-Skript** vor jeder Löschung; es muss die zu löschende Methode als
`clean` führen, und nach Aufgabe 6 `0 of N` melden.
- Ein **Anwendungsstart nach jeder Löschscheibe**, weil kein Test beantwortet, ob ein
entfernter Listener gefehlt hat — bei den Zuführungen mit konkreter Bedienhandlung
(Station wählen, Verbindung wechseln, Sked eintreffen lassen).
- Die bestehenden Feed-Tests (`TimelineFeedTest`, `SelectedStationMessagesFeedTest`,
`ConnectionStateFeedTest`, `FeedIsTheOnlyWriterTest`,
`FeedLifetimeIsTiedToTheWindowTest`) und der neue Zähler-Test aus Aufgabe 1.
## Was unverändert bleibt
- Die Fachlogik in `core` und sämtliche Protokollformate.
- Die **Schnittstelle** `UiDispatcher` und ihre Umsetzung `JavaFxUiDispatcher` — der
Tausch ist Etappe 4.
- Die Invarianten aus `AGENTS.md`: `ObservableRoster` ist der Speicher, die Listen sind
Spiegel; kein Worker-Faden fasst UI-Sammlungen an; Zoom, Auswahl, Fokus, Sortierung und
vorbelegter Text ändern sich nicht als Nebeneffekt.
- Die 91 MP3-Dateien und die lebende CW-Ausgabe in `PlayAudioUtils`.
- `TimelineView.java`, `PathProfileChart.java`, `StationMapView.java` und die übrigen
JavaFX-Dateien als Dateien — nur ihre Nutzung in `Kst4ContestApplication` fällt, soweit
diese Scheiben sie berühren.
- Die vorgefundene System-Doppelzustellung der Nachrichtentabelle (`refresh()` +
nachgezogener `push`) — sie wird in Aufgabe 1 festgehalten, nicht aufgelöst.
## Risiken
**Eine Löschung nimmt der Compose-Oberfläche still einen Anstoß.** Das ist die
Fehlerklasse, der dieser ganze Plan gilt. Jede Scheibe hat deshalb einen Anwendungsstart
als Abnahme, nicht nur einen grünen Build.
**Die Zeilennummern in diesem Plan altern mit jeder Scheibe.** Jede Aufgabe erhebt ihre
Stellen vor der Arbeit neu per Grep; keine Löschung stützt sich auf eine hier notierte
Zeile, nur auf den Namen.
**`start()` ist 2.000+ Zeilen durchsetzter Aufbau.** Aufgabe 7 ist zeilenweise und
einzeln zu prüfen; ein am Stück beschnittenes `start()` ist der sichere Weg, einen
lebenden Auslöser zu verlieren.
## Was dieser Plan nicht tut
Alles Folgende ist **nicht** Teil 3, sondern ein späterer Plan (Spec-Etappen 4–6), weil
es erst schreibbar ist, wenn `start()` zerlegt und der durchsetzte Aufbau gelöscht ist:
- **Etappe 4:** `JavaFxUiDispatcher` → eine Umsetzung auf dem AWT-Ereignisfaden. Die
Schnittstelle `UiDispatcher` bleibt, nur ihre Umsetzung wird getauscht.
- **Etappe 5:** der Lebenszyklus (`extends Application`, `launch(args)`, `start(Stage)` →
einfaches `main`), die zehn `Alert`-Dialoge als eigene Compose-Fenster, Fensterrahmen,
Startgröße (`Screen`) und Anwendungssymbol, und der macOS-Systemmenüleisten-Pfad.
- **Etappe 6:** die Restdateien (`StationMapView`, `StationMapBridge`, `MapHtmlResources`,
Leaflet-Ressourcen, `TileProxyServer`, `PathProfileChart`, `TimelineView`,
`MessageTextTableCell`, `TruncatedTextTableCell`, `JavaFxUiDispatcher`), das Verschieben
von `MessageVariableResolverTest` nach `src/test`, das Entfernen der `javafx`-Importe
aus `TableLayoutManager.java` (die Klasse **bleibt**, sie wird von sechs Compose-Dateien
benutzt), und das Befreien des Builds — der `org.openjfx`-Block aus
`app-desktop/build.gradle.kts` und `gradle/libs.versions.toml`.
Erst wenn `grep -rl "javafx\." core/src/main app-desktop/src/main` nichts mehr liefert und
der `org.openjfx`-Block aus dem Build fällt, ist das Ziel der Spezifikation erreicht — das
ist das Ende von Etappe 6, nicht von Teil 3.