4 Commits
Author SHA1 Message Date
github-actions[bot] 3988a6d456 chore: rebuild website [skip ci] 2026-07-12 21:52:34 +00:00
Marc Froehlich 598be64306 modify interactive aircraft scatter hero effect 2026-07-12 23:52:11 +02:00
github-actions[bot] 7fcea718ec chore: rebuild website [skip ci] 2026-07-12 21:23:10 +00:00
praktimarc fd7d0a6807 Bugfix/51 selectionmodel fix (#60)
* bugfix for #51. ManagebusManagementThread now never accesses or changes the JAVAFX view-backing ObservableList but using a concurrenthasmap which drives the Tableview backing list with snapshots

* Extending work at #51 for not adding users on UM3 message

* Add interactive aircraft scatter hero effect
2026-07-12 23:22:53 +02:00
54 changed files with 3423 additions and 912 deletions
@@ -24,6 +24,7 @@ import kst4contest.logic.PriorityCalculator;
import kst4contest.model.*;
import kst4contest.test.MockKstServer;
import kst4contest.utils.PlayAudioUtils;
import kst4contest.locatorUtils.Location;
import kst4contest.view.Kst4ContestApplication;
import java.io.*;
@@ -34,6 +35,7 @@ import java.util.function.Predicate;
/**
*
* Central Chat kst4contest.controller. Instantiate only one time per category of kst Chat.
@@ -318,10 +320,8 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
EnumSet<Band> offered = EnumSet.noneOf(Band.class);
synchronized (getLst_chatMemberList()) {
for (ChatMember cm : getLst_chatMemberList()) {
for (ChatMember cm : findActiveChatMembersByRawCall(callRaw)) {
if (cm == null || cm.getCallSignRaw() == null) continue;
if (!normalizeCallRaw(cm.getCallSignRaw()).equals(callRaw)) continue;
Map<Band, ChatMember.ActiveFrequencyInfo> map = cm.getKnownActiveBands();
if (map == null || map.isEmpty()) continue;
@@ -340,11 +340,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
+ " freq=" + e.getValue().frequency
+ " ageMs=" + age);
}
}
}
}
}
}
return offered;
}
@@ -356,10 +354,8 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
EnumSet<Band> worked = EnumSet.noneOf(Band.class);
synchronized (getLst_chatMemberList()) {
for (ChatMember cm : getLst_chatMemberList()) {
for (ChatMember cm : findActiveChatMembersByRawCall(callRaw)) {
if (cm == null || cm.getCallSignRaw() == null) continue;
if (!normalizeCallRaw(cm.getCallSignRaw()).equals(callRaw)) continue;
if (cm.isWorked144()) worked.add(Band.B_144);
if (cm.isWorked432()) worked.add(Band.B_432);
@@ -368,9 +364,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
if (cm.isWorked3400()) worked.add(Band.B_3400);
if (cm.isWorked5600()) worked.add(Band.B_5760);
if (cm.isWorked10G()) worked.add(Band.B_10G);
if (cm.isWorked24G()) worked.add(Band.B_24G); // optional, only if your Band enum supports it
}
if (cm.isWorked24G()) worked.add(Band.B_24G);
}
return worked;
}
@@ -645,7 +641,8 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
// rotatorClient.
this.lst_chatMemberList.clear();;
// this.lst_chatMemberList.clear();;
this.clearActiveChatMembers();
this.lst_clusterMemberList.clear();
this.setDisconnected(true);
@@ -713,7 +710,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
}
} else if (action.equals(ApplicationConstants.DISCSTRING_DISCONNECTONLY)){
this.lst_chatMemberList.clear();;
this.clearActiveChatMembers();
this.lst_clusterMemberList.clear();
@@ -959,16 +956,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
return null;
}
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
synchronized (getLst_chatMemberList()) {
for (ChatMember member : getLst_chatMemberList()) {
if (member == null || member.getCallSignRaw() == null) continue;
if (normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) {
return member;
}
}
}
return null;
List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
}
private String resolveSkedTargetLocator(String targetCallsignRaw) {
@@ -976,22 +966,43 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
return null;
}
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
synchronized (getLst_chatMemberList()) {
for (ChatMember member : getLst_chatMemberList()) {
if (member == null || member.getCallSignRaw() == null) continue;
if (!normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) continue;
for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
String locator = member.getQra();
if (locator != null && !locator.isBlank()) {
return locator.trim().toUpperCase(Locale.ROOT);
}
}
}
return null;
}
// private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
// return null;
// }
//
// List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
// return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
//
// }
//
// private String resolveSkedTargetLocator(String targetCallsignRaw) {
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
// return null;
// }
//
// String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
//
// for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
// String locator = member.getQra();
// if (locator != null && !locator.isBlank()) {
// return locator.trim().toUpperCase(Locale.ROOT);
// }
// }
//
// return null;
// }
public StationMetricsService getStationMetricsService() {
return stationMetricsService;
}
@@ -1014,10 +1025,14 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
return new HashMap<>(lastInboundCategoryByCallSignRaw);
}
// public List<ChatMember> snapshotChatMembers() {
// synchronized (getLst_chatMemberList()) {
// return new ArrayList<>(getLst_chatMemberList());
// }
// }
public List<ChatMember> snapshotChatMembers() {
synchronized (getLst_chatMemberList()) {
return new ArrayList<>(getLst_chatMemberList());
}
return new ArrayList<>(activeChatMembersByCallAndCategory.values());
}
public List<ContestSked> snapshotActiveSkeds() {
@@ -1126,13 +1141,27 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
private ObservableList<ChatMember> chatMemberList = FXCollections.observableArrayList(); // List of active stations
private ObservableList<ChatMember> lst_chatMemberList = FXCollections.synchronizedObservableList(chatMemberList); // List
// of active stn in chat
private FilteredList<ChatMember> lst_chatMemberListFiltered = new FilteredList<ChatMember>(chatMemberList);
private SortedList<ChatMember> lst_chatMemberSortedFilteredList = new SortedList<ChatMember>(lst_chatMemberListFiltered);
private ObservableList<ChatMember> lst_chatMemberList = FXCollections.synchronizedObservableList(chatMemberList); // List
private ObservableList<Predicate<ChatMember>> lst_chatMemberListFilterPredicates = FXCollections.observableArrayList();
private ObservableList<ClusterMessage> lst_clusterMemberList = FXCollections.observableArrayList();
/*
* Thread-safe active-member model.
*
* MessageBusManagementThread must not use the JavaFX TableView backing list as
* its primary data model. Otherwise every add/remove fires FilteredList,
* SortedList and TableView selection listeners on the MessageBus thread and JavaFX
* can throw "Not on FX application thread" or internal listener NPEs.
*
* This map is the worker-thread safe source of truth. The ObservableList below
* remains a UI mirror and is mutated only on the JavaFX application thread.
*/
private final java.util.concurrent.ConcurrentMap<String, ChatMember> activeChatMembersByCallAndCategory =
new java.util.concurrent.ConcurrentHashMap<>();
/*
* Message table update buffers.
@@ -1161,6 +1190,254 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
// ******************************************************************************************************************************************
/**
* Executes UI-bound work on the JavaFX application thread.
*
* Worker threads may call this safely. If the caller is already on the FX
* thread, the action is executed immediately to preserve ordering.
*/
private void runOnFxThread(Runnable action) {
if (action == null) {
return;
}
if (Platform.isFxApplicationThread()) {
action.run();
} else {
Platform.runLater(action);
}
}
/**
* Builds the active-member key. The raw/base callsign alone is not enough
* because the same station can be logged into multiple ON4KST categories at
* the same time. Therefore the category number is part of the key.
*/
private String buildActiveChatMemberKey(String callSign, ChatCategory category) {
String normalizedCallsign = ChatMember.normalizeCallSignToBaseCallSign(callSign);
if (normalizedCallsign == null || normalizedCallsign.isBlank()) {
return null;
}
int categoryNumber = category == null ? -1 : category.getCategoryNumber();
return normalizedCallsign.trim().toUpperCase(Locale.ROOT) + "|" + categoryNumber;
}
private String buildActiveChatMemberKey(ChatMember member) {
if (member == null) {
return null;
}
String callSign = member.getCallSignRaw() != null ? member.getCallSignRaw() : member.getCallSign();
return buildActiveChatMemberKey(callSign, member.getChatCategory());
}
/**
* Adds or replaces an active chat member in the worker-thread model and mirrors
* that change to the JavaFX list. This is the only supported path for ON4KST
* user-enter events.
*/
public void addOrUpdateActiveChatMember(ChatMember member) {
String key = buildActiveChatMemberKey(member);
if (key == null) {
return;
}
activeChatMembersByCallAndCategory.put(key, member);
runOnFxThread(() -> {
int existingIndex = findChatMemberIndexInUiListByKey(key);
if (existingIndex >= 0) {
lst_chatMemberList.set(existingIndex, member);
} else {
lst_chatMemberList.add(member);
}
});
}
/**
* Removes an active chat member from the worker-thread model and from the UI
* mirror. Removal from the ObservableList is always performed on the FX thread
* because the list is bound to FilteredList/SortedList/TableView.
*/
public boolean removeActiveChatMember(ChatMember member) {
String key = buildActiveChatMemberKey(member);
if (key == null) {
return false;
}
ChatMember removedMember = activeChatMembersByCallAndCategory.remove(key);
runOnFxThread(() -> lst_chatMemberList.removeIf(currentMember -> key.equals(buildActiveChatMemberKey(currentMember))));
return removedMember != null;
}
/**
* Clears the active-member model and the JavaFX UI mirror. Use this instead of
* getLst_chatMemberList().clear() from timers or worker threads.
*/
public void clearActiveChatMembers() {
activeChatMembersByCallAndCategory.clear();
runOnFxThread(() -> lst_chatMemberList.clear());
}
/**
* Resolves a member from the thread-safe active model. This avoids reading the
* TableView backing list from MessageBusManagementThread.
*/
public ChatMember findActiveChatMember(ChatMember lookForThis) {
String key = buildActiveChatMemberKey(lookForThis);
return key == null ? null : activeChatMembersByCallAndCategory.get(key);
}
public ChatMember findActiveChatMember(String callSign, ChatCategory category) {
String key = buildActiveChatMemberKey(callSign, category);
return key == null ? null : activeChatMembersByCallAndCategory.get(key);
}
public int getActiveChatMemberCount() {
return activeChatMembersByCallAndCategory.size();
}
/**
* Returns all active category variants of a callsign. This is used when a QRG or
* band hint should be propagated from one category instance to the same station
* in another category.
*/
public List<ChatMember> findActiveChatMembersByRawCall(String callSignRaw) {
String normalizedCallsign = ChatMember.normalizeCallSignToBaseCallSign(callSignRaw);
if (normalizedCallsign == null || normalizedCallsign.isBlank()) {
return Collections.emptyList();
}
String normalizedKeyPart = normalizedCallsign.trim().toUpperCase(Locale.ROOT);
List<ChatMember> matchingMembers = new ArrayList<>();
for (ChatMember member : activeChatMembersByCallAndCategory.values()) {
if (member == null) {
continue;
}
String memberCall = member.getCallSignRaw() != null ? member.getCallSignRaw() : member.getCallSign();
String normalizedMemberCall = ChatMember.normalizeCallSignToBaseCallSign(memberCall);
if (normalizedMemberCall != null && normalizedMemberCall.equalsIgnoreCase(normalizedKeyPart)) {
matchingMembers.add(member);
}
}
return matchingMembers;
}
/**
* Updates locator and derived direction values in the active model. The UI list
* contains the same member object, but the user-list refresh is still triggered
* on the JavaFX thread so TableView/map derived displays can repaint safely.
*/
public boolean updateActiveChatMemberLocator(ChatMember lookupMember, String newLocator) {
ChatMember activeMember = findActiveChatMember(lookupMember);
if (activeMember == null) {
return false;
}
activeMember.setQra(newLocator);
activeMember.setQrb(new Location().getDistanceKmByTwoLocatorStrings(chatPreferences.getStn_loginLocatorMainCat(), newLocator));
activeMember.setQTFdirection(new Location(chatPreferences.getStn_loginLocatorMainCat()).getBearing(new Location(newLocator)));
fireUserListUpdate("Locator changed");
return true;
}
public boolean updateActiveChatMemberState(ChatMember lookupMember, int newState) {
ChatMember activeMember = findActiveChatMember(lookupMember);
if (activeMember == null) {
return false;
}
activeMember.setState(newState);
fireUserListUpdate("User state changed");
return true;
}
/**
* Applies a UM3-style user-info update only to an already active chat member.
*
* ON4KST may send UM3 messages for stations that are not logged into the
* current channel, for example when a user changes locator/profile data on the
* website. Such stations must not be added to the visible chat member list,
* because they cannot be addressed in the channel. Their full information will
* be delivered again with UA0/UA5/UA2 when they actually join.
*
* @return true if an active member was found and updated, false if the UM3
* information was intentionally ignored.
*/
public boolean updateActiveChatMemberInfoIfPresent(ChatMember updatedMember) {
String key = buildActiveChatMemberKey(updatedMember);
if (key == null) {
return false;
}
ChatMember activeMember = activeChatMembersByCallAndCategory.get(key);
if (activeMember == null) {
return false;
}
activeMember.setName(updatedMember.getName());
activeMember.setQra(updatedMember.getQra());
activeMember.setState(updatedMember.getState());
activeMember.setLastActivity(updatedMember.getLastActivity());
activeMember.setActivityTimeLastInEpoch(updatedMember.getActivityTimeLastInEpoch());
activeMember.setQrb(updatedMember.getQrb());
activeMember.setQTFdirection(updatedMember.getQTFdirection());
fireUserListUpdate("User info updated");
return true;
}
/**
* Backward-compatible wrapper for older call sites. Despite the historic name,
* this method no longer adds unknown UM3 users to the active member model.
*/
public boolean updateOrAddActiveChatMemberInfo(ChatMember updatedMember) {
return updateActiveChatMemberInfoIfPresent(updatedMember);
}
/**
* Stores a newly detected QRG/band on all active category variants of a station.
* The old StringProperty is still set for compatibility with existing TableView
* columns and DXCluster code.
*/
public void applyDetectedFrequencyToActiveMembers(ChatMember sender, Band detectedBand, double detectedFrequencyMhz) {
if (sender == null || detectedBand == null || detectedFrequencyMhz <= 0) {
return;
}
String displayFrequency = String.valueOf(detectedFrequencyMhz);
String rawCall = sender.getCallSignRaw() != null ? sender.getCallSignRaw() : sender.getCallSign();
for (ChatMember member : findActiveChatMembersByRawCall(rawCall)) {
if (member == null) {
continue;
}
member.addKnownFrequency(detectedBand, detectedFrequencyMhz);
member.setFrequency(new SimpleStringProperty(displayFrequency));
}
// Also cover dummy senders that are not in the active model, e.g. [n/a] senders.
sender.addKnownFrequency(detectedBand, detectedFrequencyMhz);
sender.setFrequency(new SimpleStringProperty(displayFrequency));
fireUserListUpdate("Frequency detected");
}
private int findChatMemberIndexInUiListByKey(String key) {
if (key == null) {
return -1;
}
for (int i = 0; i < lst_chatMemberList.size(); i++) {
if (key.equals(buildActiveChatMemberKey(lst_chatMemberList.get(i)))) {
return i;
}
}
return -1;
}
@@ -2011,7 +2288,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
socket.close();
chatController.setConnectedAndLoggedIn(false);
chatController.getLst_chatMemberList().clear();
chatController.clearActiveChatMembers();
System.out.println("[Chatcontroller, Warning: ] Socket closed or disconnected");
@@ -34,9 +34,9 @@ public class MessageBusManagementThread extends Thread {
private ThreadStatusCallback callBackToController;
private PrintWriter writer;
// private Socket socket;
// private Socket socket;
private ChatController client;
// private File fileLogRAW;
// private File fileLogRAW;
// private TimerTask userActualizationTask; // Is used as a temporary userout-print
// private TimerTask userActualizationTask; //kst4contest.test 4 23001
// private boolean serverReady = false; //kst4contest.test 4 23001
@@ -324,31 +324,12 @@ public class MessageBusManagementThread extends Thread {
// --- STEP 3: Process Result ---
if (finalDetectedBand != null && finalDetectedFrequency > 0) {
// 1. Store in the new Map (for future context/history)
sender.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
// No automatic full terrain analysis here.
// The frequency is stored for later map/manual reachability requests.
//propagate known frequency to all instances of the same callsign (callRaw may exist multiple times)
try {
ArrayList<Integer> sameCallIdx = client.checkListForChatMemberIndexesByCallSign(sender);
for (int idx : sameCallIdx) {
ChatMember cm = client.getLst_chatMemberList().get(idx);
if (cm != null && cm != sender) {
cm.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
// No automatic full terrain analysis here.
// Avoids exhausting the online terrain API when many stations mention QRGs.
}
}
} catch (Exception e) {
System.out.println("[SmartParser, warning]: failed to propagate known frequency across duplicates: " + e.getMessage());
}
// 2. Set the old String-Property for GUI compatibility
// We assume standard display format (MHz)
sender.setFrequency(new javafx.beans.property.SimpleStringProperty(String.valueOf(finalDetectedFrequency)));
/*
* Store the detected QRG in the thread-safe active-member model.
* The UI table is only a JavaFX mirror, so the MessageBus must not scan
* or mutate getLst_chatMemberList() here.
*/
client.applyDetectedFrequencyToActiveMembers(sender, finalDetectedBand, finalDetectedFrequency);
System.out.println("[SmartParser] Detected for " + sender.getCallSign() + ": " +
finalDetectedFrequency + " MHz (" + finalDetectedBand + ") " +
@@ -491,6 +472,92 @@ public class MessageBusManagementThread extends Thread {
return -1; // if it´s not found, the method will always end here and return -1
}
/**
* Creates a minimal receiver object for ON4KST broadcast messages.
* Keeping this as an object, not null, protects all downstream chat filters
* and debug logging from null receiver paths.
*/
private ChatMember createAllReceiver() {
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
dummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
return dummy;
}
/**
* Resolves a message sender from the thread-safe active-member model. If a
* CH/CR message arrives before the matching user-enter message, a marked
* fallback sender is used. The returned sender is never null.
*/
private ChatMember resolveInboundSender(String senderCallSign, ChatCategory category, ChatMessage message) {
ChatMember lookup = new ChatMember();
lookup.setCallSign(senderCallSign);
lookup.setChatCategory(category);
ChatMember senderObj = this.client.findActiveChatMember(lookup);
if (senderObj != null) {
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// Remember the last active category so later outgoing replies can be routed correctly.
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
// Metrics influence priority scoring; process them after message text is known.
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
message == null ? null : message.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
);
this.client.getScoreService().requestRecompute("rx-chat-message");
return senderObj;
}
ChatMember fallbackSender = new ChatMember();
String myCall = this.client.getChatPreferences().getStn_loginCallSign();
if (senderCallSign != null && senderCallSign.equalsIgnoreCase(myCall)) {
fallbackSender.setCallSign(myCall);
} else {
fallbackSender.setCallSign("[n/a]" + senderCallSign);
}
fallbackSender.setChatCategory(category);
fallbackSender.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
return fallbackSender;
}
/**
* Resolves a message receiver from the active-member model. Unknown receivers
* are represented as explicit fallback objects instead of null. This keeps PM
* echo display and historic messages stable even if the target station already
* left the chat.
*/
private ChatMember resolveInboundReceiver(String receiverCallSign, ChatCategory category) {
if (receiverCallSign == null || receiverCallSign.equals("0")) {
return createAllReceiver();
}
ChatMember lookup = new ChatMember();
lookup.setCallSign(receiverCallSign);
lookup.setChatCategory(category);
ChatMember receiverObj = this.client.findActiveChatMember(lookup);
if (receiverObj != null) {
return receiverObj;
}
ChatMember fallbackReceiver = new ChatMember();
String myCall = this.client.getChatPreferences().getStn_loginCallSign();
if (receiverCallSign.equalsIgnoreCase(myCall)) {
fallbackReceiver.setCallSign(myCall);
} else {
fallbackReceiver.setCallSign(receiverCallSign + "(left)");
}
fallbackReceiver.setChatCategory(category);
fallbackReceiver.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
return fallbackReceiver;
}
/**
* Processes received messages via port 23001 (improved telnet Interface)
*
@@ -590,7 +657,7 @@ public class MessageBusManagementThread extends Thread {
if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) {
this.client.getLst_chatMemberList().add(newMember); //the own call will not be in the list
this.client.addOrUpdateActiveChatMember(newMember); // the own call will not be in the list
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
// Reachability is calculated on demand only: map click, selected station, or manual request.
}
@@ -637,7 +704,7 @@ public class MessageBusManagementThread extends Thread {
newMember = this.client.getDbHandler().fetchChatMemberWkdDataForOnlyOneCallsignFromDB(newMember);
this.client.getLst_chatMemberList().add(newMember);
this.client.addOrUpdateActiveChatMember(newMember);
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
this.client.getDbHandler().storeChatMember(newMember);
@@ -665,17 +732,11 @@ public class MessageBusManagementThread extends Thread {
newMember.setCallSign(splittedMessageLine[2]);
System.out.println("[MSGBUSMGT, Info:] User left Chat and will be removed from list ["
+ this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign());
try {
this.client.getLst_chatMemberList().remove(
checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), newMember));
+ this.client.getActiveChatMemberCount() + "] :" + newMember.getCallSign());
//since 1.26 new method design to detect chatcategory, too!
} catch (Exception e) {
System.out.println("[MSGBUSMGT, EXC!, Error:] User sent left chat but had not been there ... ["
+ this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign() + "\n"
+ e.getStackTrace());
if (!this.client.removeActiveChatMember(newMember)) {
System.out.println("[MSGBUSMGT, Info:] User sent left chat but was not active: "
+ newMember.getCallSign() + " / " + newMember.getChatCategory());
}
} else
@@ -695,9 +756,12 @@ public class MessageBusManagementThread extends Thread {
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
newMessageArrived.setMessageText(splittedMessageLine[6]);
if (splittedMessageLine[3].equals("SERVER")) {
ChatMember dummy = new ChatMember();
dummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
dummy.setCallSign("SERVER");
dummy.setName("Sysop");
newMessageArrived.setSender(dummy);
@@ -707,77 +771,16 @@ public class MessageBusManagementThread extends Thread {
} else {
ChatMember sender = new ChatMember();
sender.setCallSign(splittedMessageLine[3]);
sender.setChatCategory(chategoryForMessageAndMessageSender);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), sender);
if (index != -1) {
//user not found in the chatmember list
try {
// newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
// this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
newMessageArrived.setSender(senderObj);
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// Remember last inbound category per callsignRaw (required for correct send-routing later)
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
// Metrics for scoring: momentum, response-time, no-reply, positive signals
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
newMessageArrived.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
);
// Activity/category changes influence priority => request recompute
this.client.getScoreService().requestRecompute("rx-chat-message");
} catch (Exception exc) {
ChatMember aSenderDummy = new ChatMember();
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
aSenderDummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
newMessageArrived.setSender(aSenderDummy);
System.out.println("MsgBusmgtT: Catched Error! " + exc.getMessage() + " // " + splittedMessageLine[3] + " is not in the list! Faking sender!");
exc.printStackTrace();
}
// b4 init list
} else {
//user not found in chatmember list, mark it, sender can not be set
if (!sender.getCallSign().equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
sender.setCallSign("[n/a]" + sender.getCallSign());
// if someone sent a message without being in the userlist (cause
// on4kst missed implementing....), callsign will be marked
} else {
//that means, message was by own station, broadcasted to all other
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
AirPlaneReflectionInfo preventNullpointerExc = new AirPlaneReflectionInfo();
preventNullpointerExc.setAirPlanesReachableCntr(0);
sender.setAirPlaneReflectInfo(preventNullpointerExc);
newMessageArrived.setSender(sender); //my own call is the sender
}
newMessageArrived.setSender(resolveInboundSender(
splittedMessageLine[3],
chategoryForMessageAndMessageSender,
newMessageArrived));
}
// newMessageArrived.setSender(this.client.getChatMemberTable().get(splittedMessageLine[3]));
}
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
newMessageArrived.setMessageText(splittedMessageLine[6]);
if (splittedMessageLine[7].equals("0")) {
// message is not directed to anyone, move it to the cq messages!
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
// message is not directed to anyone, move it to the cq messages.
newMessageArrived.setReceiver(createAllReceiver());
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
@@ -786,36 +789,9 @@ public class MessageBusManagementThread extends Thread {
} else {
//message is directed to another chatmember, process as such!
ChatMember receiver = new ChatMember();
receiver.setChatCategory(chategoryForMessageAndMessageSender); //got out of message itself
receiver.setCallSign(splittedMessageLine[7]);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), receiver);
if (index != -1) {
newMessageArrived.setReceiver(this.client.getLst_chatMemberList().get(index));// -1: Member left Chat
// before...
} else { //found in active member list
if (receiver.getCallSign().equals(client.getChatPreferences().getStn_loginCallSign())) {
/**
* If mycallsign sent a message to the server, server will publish that message and
* send it to all chatmember including me.
* As mycall is not in the userlist, the message would not been displayed if I handle
* it in the next case (marking left user, just for information). But I want an echo.
*/
receiver.setCallSign(client.getChatPreferences().getStn_loginCallSign());
newMessageArrived.setReceiver(receiver);
} else {
//this are user which left chat but had been adressed by this message
receiver.setCallSign(receiver.getCallSign() + "(left)");
newMessageArrived.setReceiver(receiver);
}
}
newMessageArrived.setReceiver(resolveInboundReceiver(
splittedMessageLine[7],
chategoryForMessageAndMessageSender));
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
@@ -979,7 +955,21 @@ public class MessageBusManagementThread extends Thread {
// the "to me message list" with modified messagetext, added rxers callsign
} else {
//message sent to other user
/*
* Message sent from one other station to another other station.
*
* The old code reached this point only with members from the visible user list.
* With explicit fallback sender/receiver objects, this path may also see
* stations that already left the chat or arrived before their user-list entry.
* In that case locators may be missing, so angle/range analysis and DX spotting
* must be skipped while the chat message itself is still published.
*/
boolean senderHasLocator = newMessageArrived.getSender().getQra() != null
&& !newMessageArrived.getSender().getQra().isBlank();
boolean receiverHasLocator = newMessageArrived.getReceiver().getQra() != null
&& !newMessageArrived.getReceiver().getQra().isBlank();
if (senderHasLocator && receiverHasLocator) {
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
newMessageArrived.getSender().getQra(),
newMessageArrived.getReceiver().getQra(),
@@ -998,7 +988,7 @@ public class MessageBusManagementThread extends Thread {
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
try {
if (newMessageArrived.getSender().getFrequency() != null) {
//TODO: testing for next version 3.33: addinitional information will be displayed in cluster if there is such an information
//TODO: testing for next version 3.33: additional information will be displayed in cluster if there is such information
ChatMember onlyForSpottingObject = new ChatMember();
onlyForSpottingObject.setCallSign(newMessageArrived.getSender().getCallSign());
onlyForSpottingObject.setFrequency(newMessageArrived.getSender().getFrequency());
@@ -1014,11 +1004,10 @@ public class MessageBusManagementThread extends Thread {
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getPotential() + "%");
}
} else {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra());
}
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject); //tells the DXCluster server to send a DXC message for this member to the logbook software
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject);
}
} catch (Exception exception) {
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
@@ -1036,14 +1025,21 @@ public class MessageBusManagementThread extends Thread {
" ; sender bearing to me: " + new Location(newMessageArrived.getSender().getQra()).getBearing(new Location(client.getChatPreferences().getStn_loginLocatorMainCat())));
newMessageArrived.getSender().setInAngleAndRange(false);
}
} else {
newMessageArrived.getSender().setInAngleAndRange(false);
System.out.println("[MSGBUSMGT, Info:] Skipping angle/range analysis for message with missing locator: "
+ newMessageArrived.getSender().getCallSign() + " -> "
+ newMessageArrived.getReceiver().getCallSign());
}
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
}
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: " + referenceDeletedByUserLeftChatDuringMessageprocessing.getStackTrace());
// referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: "
+ referenceDeletedByUserLeftChatDuringMessageprocessing.getMessage());
referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
}
// sdtout to me message-List
@@ -1062,7 +1058,8 @@ public class MessageBusManagementThread extends Thread {
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
} catch (Exception exceptionOccured) {
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage());
exceptionOccured.printStackTrace();
}
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
@@ -1085,24 +1082,21 @@ public class MessageBusManagementThread extends Thread {
temp4.setQra(splittedMessageLine[3]);
temp4.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), temp4);
if (index != -1) {
ChatMember foundThisInChatMemberList = this.client.findActiveChatMember(temp4);
if (foundThisInChatMemberList == null && this.client.getChatCategorySecondChat() != null) {
/*
* LOC messages do not carry the category. The old implementation checked
* only the main category. Try the second active category as a fallback so
* dual-channel operation can still update locator changes.
*/
temp4.setChatCategory(this.client.getChatCategorySecondChat());
foundThisInChatMemberList = this.client.findActiveChatMember(temp4);
}
if (foundThisInChatMemberList != null) {
System.out.println("[MSGBUSMGT:] Locator Change of [" + (splittedMessageLine[2] + "], old was: "
+ this.client.getLst_chatMemberList().get(index).getQra() + " new is: "
+ splittedMessageLine[3]));
ChatMember foundThisInChatMemberList = this.client.getLst_chatMemberList().get(index); //make less list accesses
// this.client.getLst_chatMemberList().get(index).setQra(splittedMessageLine[3]);
// this.client.getLst_chatMemberList().get(index).setQrb(new Location().getDistanceKmByTwoLocatorStrings(client.getChatPreferences().getLoginLocator(), splittedMessageLine[3]));
// this.client.getLst_chatMemberList().get(index).setQTFdirection(new Location(client.getChatPreferences().getLoginLocator()).getBearing(new Location(splittedMessageLine[3])));
foundThisInChatMemberList.setQra(splittedMessageLine[3]);
foundThisInChatMemberList.setQrb(new Location().getDistanceKmByTwoLocatorStrings(client.getChatPreferences().getStn_loginLocatorMainCat(), splittedMessageLine[3]));
foundThisInChatMemberList.setQTFdirection(new Location(client.getChatPreferences().getStn_loginLocatorMainCat()).getBearing(new Location(splittedMessageLine[3])));
+ foundThisInChatMemberList.getQra() + " new is: " + splittedMessageLine[3]));
this.client.updateActiveChatMemberLocator(temp4, splittedMessageLine[3]);
} else {
System.out.println("[MSGBUSMGT:] ERROR! Locator Change of ["
+ (splittedMessageLine[2] + "] is not possible, user is not in the Table!"));
@@ -1232,11 +1226,9 @@ public class MessageBusManagementThread extends Thread {
stateChangeMember.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(),
stateChangeMember);
if (index != -1 && index != 0) {
this.client.getLst_chatMemberList().get(index).setState(stateChangeMember.getState());
if (!this.client.updateActiveChatMemberState(stateChangeMember, stateChangeMember.getState())) {
System.out.println("[MSGBUSMGT, Info:] State change for inactive user: "
+ stateChangeMember.getCallSign() + " / " + stateChangeMember.getChatCategory());
}
// this.client.getChatMemberTable().get(stateChangeMember.getCallSign())
@@ -1249,31 +1241,43 @@ public class MessageBusManagementThread extends Thread {
*/
if (splittedMessageLine[0].contains(USERINFOUPDATEORUSERISBACK)) {
if (splittedMessageLine.length < 6) {
System.out.println("[MSGBUSMGT, warning:] Malformed UM3 message ignored: "
+ messageToProcess.getMessageText());
} else {
ChatMember stateChangeMember = new ChatMember();
stateChangeMember.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
stateChangeMember.setCallSign(splittedMessageLine[2]);
stateChangeMember.setName(splittedMessageLine[3]);
stateChangeMember.setQra(splittedMessageLine[4]);
stateChangeMember.setState(Integer.parseInt(splittedMessageLine[5]));
stateChangeMember.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
stateChangeMember.setQrb(new Location().getDistanceKmByTwoLocatorStrings(client.getChatPreferences().getStn_loginLocatorMainCat(), stateChangeMember.getQra()));
stateChangeMember.setQTFdirection(new Location(client.getChatPreferences().getStn_loginLocatorMainCat()).getBearing(new Location(stateChangeMember.getQra())));
stateChangeMember.setQrb(new Location().getDistanceKmByTwoLocatorStrings(
client.getChatPreferences().getStn_loginLocatorMainCat(),
stateChangeMember.getQra()));
stateChangeMember.setQTFdirection(new Location(client.getChatPreferences().getStn_loginLocatorMainCat())
.getBearing(new Location(stateChangeMember.getQra())));
this.client.getDbHandler().storeChatMember(stateChangeMember); // TODO: not clean, it should be an
// upodate
// System.out.println("[MSGBUSMGT:] DXCluster Message detected ");
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(),
stateChangeMember);
//-1 could be the case if mycall is processed
if (index != -1) {
this.client.getLst_chatMemberList().get(index).setName(stateChangeMember.getName());
this.client.getLst_chatMemberList().get(index).setQra(stateChangeMember.getQra());
this.client.getLst_chatMemberList().get(index).setState(stateChangeMember.getState());
/*
* UM3 is only an info/profile update. ON4KST also sends it for stations
* that are not logged into this channel. Such stations must not become
* visible chat members, otherwise the user could try to address an
* offline callsign and receive server-side errors.
*
* Therefore UM3 updates existing active members only. Unknown UM3 calls
* are intentionally ignored; if the station joins later, UA0/UA5/UA2
* will deliver the complete current user information again.
*/
if (!client.getChatPreferences().getStn_loginCallSign().equalsIgnoreCase(stateChangeMember.getCallSign())) {
boolean updatedActiveMember = this.client.updateActiveChatMemberInfoIfPresent(stateChangeMember);
if (updatedActiveMember) {
this.client.getDbHandler().storeChatMember(stateChangeMember); // TODO: not clean, it should be an update
} else {
System.out.println("[MSGBUSMGT, Info:] UM3 ignored for inactive user: "
+ stateChangeMember.getCallSign() + " / " + stateChangeMember.getChatCategory());
}
}
}
} else
@@ -1297,6 +1301,8 @@ public class MessageBusManagementThread extends Thread {
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
newMessageArrived.setMessageText(splittedMessageLine[6]);
if (splittedMessageLine[3].equals("SERVER")) {
ChatMember dummy = new ChatMember();
@@ -1309,76 +1315,16 @@ public class MessageBusManagementThread extends Thread {
} else {
ChatMember sender = new ChatMember();
sender.setCallSign(splittedMessageLine[3]);
sender.setChatCategory(chategoryForMessageAndMessageSender);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), sender);
if (index != -1) {
//user not found in the chatmember list
try {
// newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
// this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
newMessageArrived.setSender(senderObj);
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// Remember last inbound category per callsignRaw (required for correct send-routing later)
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
// Metrics for scoring: momentum, response-time, no-reply, positive signals
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
newMessageArrived.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
);
// Activity/category changes influence priority => request recompute
this.client.getScoreService().requestRecompute("rx-chat-message");
} catch (Exception exc) {
ChatMember aSenderDummy = new ChatMember();
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
aSenderDummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
newMessageArrived.setSender(aSenderDummy);
System.out.println("MsgBusmgtT: Catched Error! " + exc.getMessage() + " // " + splittedMessageLine[3] + " is not in the list! Faking sender!");
exc.printStackTrace();
}
// b4 init list
} else {
//user not found in chatmember list, mark it, sender can not be set
if (!sender.getCallSign().equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
sender.setCallSign("[n/a]" + sender.getCallSign());
// if someone sent a message without being in the userlist (cause
// on4kst missed implementing....), callsign will be marked
} else {
//that means, message was by own station, broadcasted to all other
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
AirPlaneReflectionInfo preventNullpointerExc = new AirPlaneReflectionInfo();
preventNullpointerExc.setAirPlanesReachableCntr(0);
sender.setAirPlaneReflectInfo(preventNullpointerExc);
newMessageArrived.setSender(sender); //my own call is the sender
}
newMessageArrived.setSender(resolveInboundSender(
splittedMessageLine[3],
chategoryForMessageAndMessageSender,
newMessageArrived));
}
// newMessageArrived.setSender(this.client.getChatMemberTable().get(splittedMessageLine[3]));
}
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
newMessageArrived.setMessageText(splittedMessageLine[6]);
if (splittedMessageLine[7].equals("0")) {
// message is not directed to anyone, move it to the cq messages!
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
// message is not directed to anyone, move it to the cq messages.
newMessageArrived.setReceiver(createAllReceiver());
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
@@ -1386,35 +1332,9 @@ public class MessageBusManagementThread extends Thread {
} else {
//message is directed to another chatmember, process as such!
ChatMember receiver = new ChatMember();
receiver.setChatCategory(chategoryForMessageAndMessageSender); //got out of message itself
receiver.setCallSign(splittedMessageLine[7]);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), receiver);
if (index != -1) {
newMessageArrived.setReceiver(this.client.getLst_chatMemberList().get(index));// -1: Member left Chat
// before...
} else { //found in active member list
if (receiver.getCallSign().equals(client.getChatPreferences().getStn_loginCallSign())) {
/**
* If mycallsign sent a message to the server, server will publish that message and
* send it to all chatmember including me.
* As mycall is not in the userlist, the message would not been displayed if I handle
* it in the next case (marking left user, just for information). But I want an echo.
*/
receiver.setCallSign(client.getChatPreferences().getStn_loginCallSign());
newMessageArrived.setReceiver(receiver);
} else {
//this are user which left chat but had been adressed by this message
receiver.setCallSign(receiver.getCallSign() + "(left)");
newMessageArrived.setReceiver(receiver);
}
}
newMessageArrived.setReceiver(resolveInboundReceiver(
splittedMessageLine[7],
chategoryForMessageAndMessageSender));
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
@@ -1447,7 +1367,16 @@ public class MessageBusManagementThread extends Thread {
} else {
//message sent to other user
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
// message sent from one other station to another other station
boolean senderHasLocator = newMessageArrived.getSender().getQra() != null
&& !newMessageArrived.getSender().getQra().isBlank();
boolean receiverHasLocator = newMessageArrived.getReceiver().getQra() != null
&& !newMessageArrived.getReceiver().getQra().isBlank();
if (senderHasLocator && receiverHasLocator) {
if (DirectionUtils.isInAngleAndRange(
client.getChatPreferences().getStn_loginLocatorMainCat(),
newMessageArrived.getSender().getQra(),
newMessageArrived.getReceiver().getQra(),
client.getChatPreferences().getStn_maxQRBDefault(),
@@ -1456,17 +1385,24 @@ public class MessageBusManagementThread extends Thread {
newMessageArrived.getSender().setInAngleAndRange(true);
} else {
newMessageArrived.getSender().setInAngleAndRange(false);
}
} else {
/*
* Historic fallback senders/receivers may not have locators.
* The message is still valid and should be displayed, but angle/range
* analysis cannot be calculated without both locators.
*/
newMessageArrived.getSender().setInAngleAndRange(false);
}
// this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
this.client.publishChatMessage(newMessageArrived); // sdtout to all message-List (new from v1.7)
this.client.publishChatMessage(newMessageArrived);
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
}
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: " + referenceDeletedByUserLeftChatDuringMessageprocessing.getStackTrace());
// referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: "
+ referenceDeletedByUserLeftChatDuringMessageprocessing.getMessage());
referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
}
// sdtout to me message-List
@@ -1479,7 +1415,8 @@ public class MessageBusManagementThread extends Thread {
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
} catch (Exception exceptionOccured) {
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage());
exceptionOccured.printStackTrace();
}
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
@@ -1534,7 +1471,7 @@ public class MessageBusManagementThread extends Thread {
for (int i = 0; i < 10; i++) {
client.getLst_globalChatMessageList().add(pwErrorMsg);
client.publishChatMessage(pwErrorMsg);
// client.getLst_toMeMessageList().add(pwErrorMsg);
// client.getLst_toAllMessageList().add(pwErrorMsg);
}
@@ -1546,6 +1483,10 @@ public class MessageBusManagementThread extends Thread {
// this.client.disconnect();
}
else if (splittedMessageLine[0].equals("DE")) {
// DXCluster delimiter/end marker; intentionally ignored.
}
else {
// bufwrtrDBGMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
@@ -1751,6 +1692,10 @@ public class MessageBusManagementThread extends Thread {
} catch (SQLException e) {
System.out.println("MsgBusMgt: process23001 went wrong / SQL Error");
e.printStackTrace();
} catch (RuntimeException e) {
System.out.println("MsgBusMgt: process23001 went wrong / Runtime Error while processing: "
+ messageTextRaw.getMessageText());
e.printStackTrace();
}
}
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="About KST4Contest">
<meta name="twitter:description" content="About KST4Contest and its contest-oriented ON4KST workflow.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -136,5 +136,8 @@
</div>
</div>
</footer>
</body>
</html>
+62
View File
@@ -11,6 +11,18 @@
--accent2: #a855f7;
--accent3: #22c55e;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
--accent: #54d63d;
--accent2: #8aef64;
--accent3: #31b54a;
--fx-plane: #79ff73;
--fx-plane-muted: rgba(121, 255, 115, 0.42);
--fx-beam: #52ff65;
--fx-beam-glow: rgba(82, 255, 101, 0.28);
--fx-reflection: #d2ffc8;
--fx-trail: rgba(121, 255, 115, 0.17);
--fx-grid: rgba(121, 255, 115, 0.055);
}
* {
@@ -25,6 +37,7 @@ html {
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 14% 12%, rgba(168, 85, 247, 0.30), transparent 28%),
radial-gradient(circle at 82% 16%, rgba(56, 189, 248, 0.24), transparent 30%),
@@ -562,3 +575,52 @@ a:hover {
grid-template-columns: 1fr;
}
}
/*
* Full-width, full-height home hero
*/
main {
width: 100%;
}
.hero.hero-split {
width: 100%;
max-width: none;
min-height: calc(100vh - 77px);
padding:
clamp(56px, 7vh, 100px)
clamp(28px, 6vw, 110px);
align-content: center;
overflow: hidden;
}
.hero.hero-split > div:first-of-type {
max-width: 760px;
}
.hero.hero-split .hero-panel {
width: min(100%, 700px);
justify-self: center;
}
@media (min-width: 1600px) {
.hero.hero-split {
grid-template-columns:
minmax(520px, 0.85fr)
minmax(600px, 1.15fr);
gap: clamp(50px, 6vw, 130px);
}
.hero.hero-split .hero-panel {
width: min(100%, 780px);
}
}
@media (max-width: 900px) {
.hero.hero-split {
min-height: auto;
padding-top: 60px;
padding-bottom: 60px;
}
}
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Contact | KST4Contest">
<meta name="twitter:description" content="Contact options for KST4Contest.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -124,5 +124,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Download KST4Contest">
<meta name="twitter:description" content="Download KST4Contest releases for Windows, Linux and macOS.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -288,5 +288,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest FAQ">
<meta name="twitter:description" content="Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -167,5 +167,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="AirScout Integration">
<meta name="twitter:description" content="AirScout information helps operators evaluate AP windows, aircraft scatter timing and candidate stations during VHF/UHF/SHF contests.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -222,5 +222,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Dual Chat Categories">
<meta name="twitter:description" content="Dual category support gives contest operators better overview when working across multiple ON4KST chat rooms.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -198,5 +198,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="DXCluster Server">
<meta name="twitter:description" content="KST4Contest includes DXCluster functionality to support situational awareness during contest operation.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -197,5 +197,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest Features">
<meta name="twitter:description" content="Contest-optimized ON4KST features for VHF, UHF and SHF operation.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -183,5 +183,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Log Synchronization">
<meta name="twitter:description" content="Log synchronization helps KST4Contest understand worked stations, active bands and contest context.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -198,5 +198,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Macros and Variables">
<meta name="twitter:description" content="Macros reduce repetitive typing and help operators respond quickly during active ON4KST contest sessions.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -197,5 +197,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Priority Score System">
<meta name="twitter:description" content="KST4Contest ranks stations by activity, direction, sked context, band information, QRG hints and operator workflow signals.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -226,5 +226,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Sked Reminder">
<meta name="twitter:description" content="KST4Contest keeps skeds in focus while the operator handles chat traffic, logging, bands and aircraft scatter timing.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -220,5 +220,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Timeline View">
<meta name="twitter:description" content="The timeline view helps operators understand short propagation windows and candidate timing during active contest operation.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -198,5 +198,8 @@
</div>
</div>
</footer>
</body>
</html>
+14 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest Contest-Optimized ON4KST Chat Client">
<meta name="twitter:description" content="KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -66,6 +66,14 @@
<main>
<section class="hero hero-split">
<canvas
class="hero-radio-fx"
data-hero-radio-fx
aria-hidden="true">
</canvas>
<div class="hero-fx-vignette" aria-hidden="true"></div>
<div>
<p class="badge">The contest-optimized ON4KST client</p>
<h1>KST4Contest</h1>
@@ -288,5 +296,10 @@
</div>
</div>
</footer>
<script src="/assets/js/hero-radio-fx.js?v=1783893151167" defer></script>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Legal Notice | KST4Contest">
<meta name="twitter:description" content="Legal notice for the KST4Contest website.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -134,5 +134,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -208,5 +208,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -210,5 +210,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -345,5 +345,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -172,5 +172,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -294,5 +294,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -201,5 +201,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest Deutsches Handbuch">
<meta name="twitter:description" content="Deutsches Benutzerhandbuch für KST4Contest.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -197,5 +197,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -316,5 +316,8 @@ flatpak install kst4contest de.x08.KST4Contest//nightly
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -252,5 +252,8 @@ Zudem wird die <strong>primäre Chat-Kategorie</strong> (z. B. IARU Region 1 VHF
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -230,5 +230,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -241,5 +241,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -208,5 +208,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -345,5 +345,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -252,5 +252,8 @@ Also, select the <strong>primary chat category</strong> (e.g., IARU Region 1 VHF
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -172,5 +172,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -293,5 +293,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -201,5 +201,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest English Manual">
<meta name="twitter:description" content="English user manual for KST4Contest.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -197,5 +197,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -316,5 +316,8 @@ flatpak install kst4contest de.x08.KST4Contest//nightly
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -230,5 +230,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -241,5 +241,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -210,5 +210,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest Manual">
<meta name="twitter:description" content="Online manual for KST4Contest, the contest-optimized ON4KST chat client.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -109,5 +109,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Hotfix Version 1.41.1">
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -110,5 +110,8 @@
</div>
</div>
</footer>
</body>
</html>
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest Website Launch">
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -114,5 +114,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest News">
<meta name="twitter:description" content="News, release updates and development notes for KST4Contest.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -124,5 +124,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Privacy Policy | KST4Contest">
<meta name="twitter:description" content="Privacy policy for the KST4Contest website.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -165,5 +165,8 @@
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest Roadmap">
<meta name="twitter:description" content="Planned enhancements for upcoming KST4Contest versions, generated from GitHub milestones and issues.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -249,5 +249,8 @@ href="/support/">
</div>
</div>
</footer>
</body>
</html>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="KST4Contest Screenshots">
<meta name="twitter:description" content="Screenshots of KST4Contest, the contest-optimized ON4KST chat client for VHF, UHF and SHF operators.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -135,5 +135,8 @@
</div>
</div>
</footer>
</body>
</html>
+23 -23
View File
@@ -10,94 +10,94 @@
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/about/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/contact/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/download/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/faq/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/airscout/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/dual-chat/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/dx-cluster/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/log-sync/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/macros/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/priority-score/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/sked-reminder/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/features/timeline/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/legal-notice/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/manual/de/airscout-integration/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/manual/de/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/manual/en/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/manual/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/privacy/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/roadmap/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/screenshots/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
<url>
<loc>https://kst4contest.hamradioonline.de/support/</loc>
<lastmod>2026-07-11</lastmod>
<lastmod>2026-07-12</lastmod>
</url>
</urlset>
+4 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:title" content="Support KST4Contest">
<meta name="twitter:description" content="Help support the ongoing development of KST4Contest.">
<link rel="stylesheet" href="/assets/css/main.css?v=1783804882235">
<link rel="stylesheet" href="/assets/css/main.css?v=1783893151167">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
@@ -267,5 +267,8 @@ rel="noopener">
</div>
</div>
</footer>
</body>
</html>
+5
View File
@@ -81,5 +81,10 @@
</div>
</div>
</footer>
{% if heroFx %}
<script src="/assets/js/hero-radio-fx.js?v={{ build.version }}" defer></script>
{% endif %}
</body>
</html>
+62
View File
@@ -11,6 +11,18 @@
--accent2: #a855f7;
--accent3: #22c55e;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
--accent: #54d63d;
--accent2: #8aef64;
--accent3: #31b54a;
--fx-plane: #79ff73;
--fx-plane-muted: rgba(121, 255, 115, 0.42);
--fx-beam: #52ff65;
--fx-beam-glow: rgba(82, 255, 101, 0.28);
--fx-reflection: #d2ffc8;
--fx-trail: rgba(121, 255, 115, 0.17);
--fx-grid: rgba(121, 255, 115, 0.055);
}
* {
@@ -25,6 +37,7 @@ html {
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 14% 12%, rgba(168, 85, 247, 0.30), transparent 28%),
radial-gradient(circle at 82% 16%, rgba(56, 189, 248, 0.24), transparent 30%),
@@ -562,3 +575,52 @@ a:hover {
grid-template-columns: 1fr;
}
}
/*
* Full-width, full-height home hero
*/
main {
width: 100%;
}
.hero.hero-split {
width: 100%;
max-width: none;
min-height: calc(100vh - 77px);
padding:
clamp(56px, 7vh, 100px)
clamp(28px, 6vw, 110px);
align-content: center;
overflow: hidden;
}
.hero.hero-split > div:first-of-type {
max-width: 760px;
}
.hero.hero-split .hero-panel {
width: min(100%, 700px);
justify-self: center;
}
@media (min-width: 1600px) {
.hero.hero-split {
grid-template-columns:
minmax(520px, 0.85fr)
minmax(600px, 1.15fr);
gap: clamp(50px, 6vw, 130px);
}
.hero.hero-split .hero-panel {
width: min(100%, 780px);
}
}
@media (max-width: 900px) {
.hero.hero-split {
min-height: auto;
padding-top: 60px;
padding-bottom: 60px;
}
}
File diff suppressed because it is too large Load Diff
+9
View File
@@ -2,9 +2,18 @@
layout: base.njk
title: KST4Contest Contest-Optimized ON4KST Chat Client
description: KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.
heroFx: true
---
<section class="hero hero-split">
<canvas
class="hero-radio-fx"
data-hero-radio-fx
aria-hidden="true">
</canvas>
<div class="hero-fx-vignette" aria-hidden="true"></div>
<div>
<p class="badge">The contest-optimized ON4KST client</p>
<h1>KST4Contest</h1>