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.
70 KiB
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 buildist 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. derderive-Generator aus Teil 1). - Scheibe B ist strukturell erledigt. Die drei Feeds werden bei
:10133–:10138erzeugt, unmittelbar nachcomposeMainWindowState = new MainWindowState(...)bei:10078— also genau in der „empfohlenen Endform" des Befunds.FeedLifetimeIsTiedToTheWindowTesthält das fest.openComposeMainWindowIfRequestedexistiert 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 auftimelineViewam Compose-Datenpfad; es schiebt bei:4839–:4846intimelineFeed, wenn der nicht null ist, und berührttimelineViewnur 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") undinitFurtherInfoAbtCallsignMSGTable(: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.ktträ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,initMenuBarin 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
updateConnectionStateIndicatorist 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 mitinitConnectionStateIndicatorButton.
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-*.xmllesen (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. ObservableRosterist der Speicher, die Listen sind Spiegel. Kein Worker-Faden fasst UI-Sammlungen an. Zustellung nur überUiDispatcher(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/resourcesbleiben — sie gehören der lebenden CW-Ausgabe inPlayAudioUtils, nicht dem totenplayCWLauncher.
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:
- 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).
- 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.
- Eine „tote" Methode war über einen Compose-Rückruf doch erreichbar. Die
Compose-Fenster rufen in
Kst4ContestApplicationzurü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 alsREACHES COMPOSEgemeldet wird. - Scheibe C löst die falsche Hälfte der Doppel-Zustellung.
refresh()und einPlatform.runLaterschieben heute beide (:320/:10381); der Listener bei:3410schiebt 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. - Die zeilenweise Zerlegung von
start()lässt einen lebenden Listener fallen. Instart()liegen totetimelineView-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. - Das Durchlauf-Skript meldet eine Methode fälschlich sauber, weil
COMPOSE_TOKENSunvollständig ist. Kommt in Teil 3 ein neues Compose-Fenster oder eine neuereplace…-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:
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_TOKENSgegen 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.
selectedCallSignInfoMessageBindingentsteht 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:
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:
/**
* 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:
./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:
./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.
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.
- 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. - Jeder verbliebene Tabellen-Builder hat genau einen Aufrufer, und der sitzt im toten
try-Block vonstart()(:6961–:9028;primaryStage.setScene(...)undprimaryStage.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. - Der lebende Compose-Pfad liegt außerhalb des toten Blocks.
openComposeMainWindowIfRequested()(erzeugtMainWindowState, die drei Feeds und diecomposeMemberListener/composeChatListener) wird nach demcatchgerufen (um:9055), ebensoopenMonitorWindow,openUpdateWindowIfAvailable,openSettingsWindowund die Zeitleisten-ErstbefüllungPlatform.runLater(this::updateTimelineVisuals)(um:9079). Der tote Szenenaufbau ist damit ein abgrenzbarertry-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:
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:
updateConnectionStateIndicatoran 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:
./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
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:
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:
./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
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:
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:
./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
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:
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:
./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
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:
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:
./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
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:
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 reinenupdateTimelineVisuals()-Auslöser machen, dentimelineView.setCurrentAntennaAzimuth-Aufruf entfernen.updateTimelineVisualsliest den Azimut ohnehin frisch aus den Preferences. -
getActiveSkeds()- unduiPulse()-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 inupdateTimelineVisualsentfernen; der Feed-Push bleibt. -
scn_ChatwindowMainScenesamt Größen-/Key-Listenern,registerThemedSceneundthemedScenesentfernen, soweit der Grep keinen lebenden Nutzer zeigt. -
Den dann leeren/toten
try { … } catch-Block instart()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 Importjavafx.scene.medianur löschen, wenn der Grep sie als unerreichbar belegt. -
TimelineView.javaselbst bleibt (Etappe 6). Diese Scheibe entfernt nur seine Nutzung. -
Step 4: Übersetzen, Suite
Run:
./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
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
coreund sämtliche Protokollformate. - Die Schnittstelle
UiDispatcherund ihre UmsetzungJavaFxUiDispatcher— der Tausch ist Etappe 4. - Die Invarianten aus
AGENTS.md:ObservableRosterist 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.javaund die übrigen JavaFX-Dateien als Dateien — nur ihre Nutzung inKst4ContestApplicationfä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:
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:
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:
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
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:
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
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:
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
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:
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:
updateConnectionStateIndicatoran 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
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:
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
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:
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–:6948zu einem reinenupdateTimelineVisuals()-Auslöser machen, dentimelineView.setCurrentAntennaAzimuth-Aufruf entfernen.updateTimelineVisualsliest den Azimut ohnehin frisch aus den Preferences (:4843), der tote Knoten brauchte ihn nicht. -
:6927und:6930bleiben unverändert — sie rufen schon nurupdateTimelineVisuals(). -
:9069bleibt — die Erstbefüllung. -
Step 3: Den toten
timelineViewund seinen eigenen Zweig löschen -
Die Erzeugung (
:6878–:6885), die beiden Größen-Listener,setCurrentAntennaAzimuth/setBeamWidthDeg,setOnCandidateClickedund den Getter entfernen. -
In
updateTimelineVisualsden toten Zweigif (timelineView != null) { timelineView.updateVisuals(...) }(:4819–:4821) entfernen; der Feed-Push darunter bleibt. -
Das Feld
private TimelineView timelineView;(:213) fällt zuletzt. -
TimelineView.javaselbst bleibt — es ist eine eigene Datei und gehört in Etappe 6 (Restdateien), nicht hierher. Diese Aufgabe entfernt nur seine Nutzung inKst4ContestApplication. -
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:
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
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
cleanführen, und nach Aufgabe 60 of Nmelden. - 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
coreund sämtliche Protokollformate. - Die Schnittstelle
UiDispatcherund ihre UmsetzungJavaFxUiDispatcher— der Tausch ist Etappe 4. - Die Invarianten aus
AGENTS.md:ObservableRosterist 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.javaund die übrigen JavaFX-Dateien als Dateien — nur ihre Nutzung inKst4ContestApplicationfällt, soweit diese Scheiben sie berühren.- Die vorgefundene System-Doppelzustellung der Nachrichtentabelle (
refresh()+ nachgezogenerpush) — 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 SchnittstelleUiDispatcherbleibt, nur ihre Umsetzung wird getauscht. - Etappe 5: der Lebenszyklus (
extends Application,launch(args),start(Stage)→ einfachesmain), die zehnAlert-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 vonMessageVariableResolverTestnachsrc/test, das Entfernen derjavafx-Importe ausTableLayoutManager.java(die Klasse bleibt, sie wird von sechs Compose-Dateien benutzt), und das Befreien des Builds — derorg.openjfx-Block ausapp-desktop/build.gradle.ktsundgradle/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.