From 9f1a6f44561281199f5027c30c94ece7b24f89db Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 11 Feb 2026 13:09:30 -0500 Subject: [PATCH 1/6] refactor: delete monolithic queue-server source files --- .../applications/queueserver/Messages.java | 17 - .../applications/queueserver/Preferences.java | 37 - .../queueserver/QueueServerApp.java | 113 -- .../queueserver/QueueServerInstance.java | 23 - .../queueserver/QueueServerMenuEntry.java | 20 - .../queueserver/api/ConsoleOutputText.java | 7 - .../queueserver/api/ConsoleOutputUid.java | 10 - .../queueserver/api/ConsoleOutputUpdate.java | 16 - .../api/ConsoleOutputWsMessage.java | 19 - .../queueserver/api/Envelope.java | 12 - .../queueserver/api/EverythingElse.java | 17 - .../queueserver/api/HistoryGetPayload.java | 9 - .../applications/queueserver/api/NoBody.java | 4 - .../queueserver/api/QueueGetPayload.java | 14 - .../queueserver/api/QueueItem.java | 17 - .../queueserver/api/QueueItemAdd.java | 37 - .../queueserver/api/QueueItemAddBatch.java | 19 - .../queueserver/api/QueueItemMove.java | 18 - .../queueserver/api/QueueItemMoveBatch.java | 20 - .../queueserver/api/StatusResponse.java | 42 - .../queueserver/api/StatusWsMessage.java | 30 - .../queueserver/api/SystemInfoWsMessage.java | 31 - .../queueserver/client/ApiEndpoint.java | 73 - .../queueserver/client/Endpoint.java | 3 - .../queueserver/client/HttpMethod.java | 4 - .../client/QueueServerWebSocket.java | 200 --- .../client/RunEngineHttpClient.java | 193 --- .../queueserver/client/RunEngineService.java | 361 ----- .../controller/ApplicationController.java | 57 - .../EditAndControlQueueController.java | 47 - .../controller/MonitorQueueController.java | 150 -- .../ReConsoleMonitorController.java | 532 ------- .../ReEnvironmentControlsController.java | 88 -- .../ReExecutionControlsController.java | 117 -- .../ReManagerConnectionController.java | 419 ----- .../controller/RePlanEditorController.java | 1398 ----------------- .../controller/RePlanHistoryController.java | 517 ------ .../controller/RePlanManagerController.java | 38 - .../controller/RePlanQueueController.java | 541 ------- .../controller/RePlanViewerController.java | 505 ------ .../controller/ReQueueControlsController.java | 132 -- .../controller/ReRunningPlanController.java | 200 --- .../controller/ReStatusMonitorController.java | 71 - .../queueserver/util/AppLifecycle.java | 65 - .../queueserver/util/HttpSupport.java | 34 - .../util/JythonScriptExecutor.java | 154 -- .../queueserver/util/PlansCache.java | 143 -- .../queueserver/util/PollCenter.java | 62 - .../util/PythonParameterConverter.java | 322 ---- .../util/QueueItemSelectionEvent.java | 103 -- .../queueserver/util/RateLimiter.java | 45 - .../queueserver/util/RunEngineCli.java | 87 - .../queueserver/util/RunEngineRepl.java | 81 - .../queueserver/util/StatusBus.java | 82 - .../queueserver/view/ItemUpdateEvent.java | 39 - .../queueserver/view/PlanEditEvent.java | 46 - .../queueserver/view/TabSwitchEvent.java | 38 - .../queueserver/view/UiSignalEvent.java | 21 - .../queueserver/view/ViewFactory.java | 39 - .../org.phoebus.framework.spi.AppDescriptor | 1 - .../services/org.phoebus.ui.spi.MenuEntry | 1 - .../src/main/resources/icons/bluesky.png | Bin 484 -> 0 bytes .../applications/queueserver/css/style.css | 46 - .../queueserver/logging.properties | 7 - .../queueserver/messages.properties | 2 - .../queueserver/messages_fr.properties | 2 - .../queueserver/scripts/type_converter.py | 186 --- .../queueserver/view/Application.fxml | 28 - .../queueserver/view/EditAndControlQueue.fxml | 50 - .../queueserver/view/MonitorQueue.fxml | 48 - .../queueserver/view/ReConsoleMonitor.fxml | 23 - .../view/ReEnvironmentControls.fxml | 35 - .../queueserver/view/ReExecutionControls.fxml | 47 - .../queueserver/view/ReManagerConnection.fxml | 32 - .../queueserver/view/RePlanEditor.fxml | 45 - .../queueserver/view/RePlanHistory.fxml | 53 - .../queueserver/view/RePlanManager.fxml | 30 - .../queueserver/view/RePlanQueue.fxml | 50 - .../queueserver/view/RePlanViewer.fxml | 42 - .../queueserver/view/ReQueueControls.fxml | 45 - .../queueserver/view/ReRunningPlan.fxml | 20 - .../queueserver/view/ReStatusMonitor.fxml | 46 - .../queueserver_preferences.properties | 40 - 83 files changed, 8418 deletions(-) delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/Messages.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/Preferences.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerApp.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerInstance.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerMenuEntry.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputText.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUid.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUpdate.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputWsMessage.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/Envelope.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/EverythingElse.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/HistoryGetPayload.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/NoBody.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueGetPayload.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItem.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAdd.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAddBatch.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMove.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMoveBatch.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusResponse.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusWsMessage.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/SystemInfoWsMessage.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/ApiEndpoint.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/Endpoint.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/HttpMethod.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/QueueServerWebSocket.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReStatusMonitorController.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/AppLifecycle.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/JythonScriptExecutor.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PlansCache.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PythonParameterConverter.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/QueueItemSelectionEvent.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RateLimiter.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineCli.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineRepl.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/UiSignalEvent.java delete mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java delete mode 100644 app/queue-server/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor delete mode 100644 app/queue-server/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry delete mode 100644 app/queue-server/src/main/resources/icons/bluesky.png delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/logging.properties delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages.properties delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages_fr.properties delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/scripts/type_converter.py delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/EditAndControlQueue.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/MonitorQueue.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReConsoleMonitor.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReEnvironmentControls.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReExecutionControls.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReManagerConnection.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanEditor.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanHistory.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanManager.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanQueue.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanViewer.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReQueueControls.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReRunningPlan.fxml delete mode 100644 app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReStatusMonitor.fxml delete mode 100644 app/queue-server/src/main/resources/queueserver_preferences.properties diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/Messages.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/Messages.java deleted file mode 100644 index 25fe16fb50..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/Messages.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.phoebus.applications.queueserver; - -import org.phoebus.framework.nls.NLS; - -/** Externalised strings for Queue-Monitor plug-in */ -@SuppressWarnings("nls") -public final class Messages { - - // ---------- keep alphabetically sorted ---------- - public static String QueueServer; // display name - public static String QueueServerMenuPath; // menu path - // ----------------------------------------------- - - static { NLS.initializeMessages(Messages.class); } - - private Messages() { /* no-instantiation */ } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/Preferences.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/Preferences.java deleted file mode 100644 index af6a18160a..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/Preferences.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2024 European Spallation Source ERIC. - */ - -package org.phoebus.applications.queueserver; - -import org.phoebus.framework.preferences.AnnotatedPreferences; -import org.phoebus.framework.preferences.Preference; - -public class Preferences { - - @Preference - public static String queue_server_url; - - @Preference - public static String api_key; - - @Preference - public static String api_key_file; - - @Preference - public static boolean debug; - - @Preference - public static int connectTimeout; - - @Preference - public static boolean use_websockets; - - @Preference - public static int update_interval_ms; - - static - { - AnnotatedPreferences.initialize(Preferences.class, "/queueserver_preferences.properties"); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerApp.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerApp.java deleted file mode 100644 index c2f14f2218..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerApp.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.phoebus.applications.queueserver; - -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.phoebus.applications.queueserver.client.RunEngineHttpClient; -import org.phoebus.applications.queueserver.util.AppLifecycle; -import org.phoebus.applications.queueserver.view.ViewFactory; -import javafx.scene.Parent; - -import org.phoebus.framework.spi.AppInstance; -import org.phoebus.framework.spi.AppResourceDescriptor; -import org.phoebus.framework.workbench.ApplicationService; -import org.phoebus.ui.docking.DockItem; -import org.phoebus.ui.docking.DockPane; - -@SuppressWarnings("nls") -public final class QueueServerApp implements AppResourceDescriptor { - - public static final Logger logger = Logger.getLogger(QueueServerApp.class.getPackageName()); - - public static final String NAME = "queue-server"; - private static final String DISPLAY_NAME = "Queue Server"; - - // Start Jython warmup as soon as plugin loads (during Phoebus startup) - // This runs before the user opens the app, reducing/eliminating UI freeze - static { - org.phoebus.applications.queueserver.util.PythonParameterConverter.initializeInBackground(); - } - - @Override public String getName() { return NAME; } - @Override public String getDisplayName() { return DISPLAY_NAME; } - - @Override public URL getIconURL() { - return getClass().getResource("/icons/bluesky.png"); // add one or reuse probe.png - } - - @Override public AppInstance create() { - // Resolve server URL with default fallback - String serverUrl = Preferences.queue_server_url; - // Check if the preference wasn't expanded (still has $(VAR) syntax) or is empty - if (serverUrl == null || serverUrl.trim().isEmpty() || serverUrl.startsWith("$(")) { - serverUrl = "http://localhost:60610"; - logger.log(Level.INFO, "Using default Queue Server URL: " + serverUrl); - } - - // Resolve API key with priority: - // 1. Direct api_key preference (or QSERVER_HTTP_SERVER_API_KEY env var) - // 2. Read from api_key_file path (or QSERVER_HTTP_SERVER_API_KEYFILE env var) - String apiKey = resolveApiKey(); - - RunEngineHttpClient.initialize(serverUrl, apiKey); - - Parent root = ViewFactory.APPLICATION.get(); - - QueueServerInstance inst = new QueueServerInstance(this, root); - - DockItem tab = new DockItem(inst, root); - // Register cleanup callback when tab is closed - tab.addClosedNotification(AppLifecycle::shutdown); - DockPane.getActiveDockPane().addTab(tab); - - return inst; - } - - /** - * Resolve the API key using the same priority as Python bluesky-widgets: - * 1. Check QSERVER_HTTP_SERVER_API_KEY environment variable (via api_key preference) - * 2. If not set, check QSERVER_HTTP_SERVER_API_KEYFILE environment variable (via api_key_file preference) - * 3. If keyfile path is set, read the API key from that file - * - * @return The resolved API key, or null if not configured - */ - private static String resolveApiKey() { - // First priority: direct API key - String apiKey = Preferences.api_key; - // Check if the preference was expanded (not still $(VAR) syntax) and not empty - if (apiKey != null && !apiKey.trim().isEmpty() && !apiKey.startsWith("$(")) { - logger.log(Level.FINE, "Using API key from QSERVER_HTTP_SERVER_API_KEY"); - return apiKey.trim(); - } - - // Second priority: read from keyfile - String keyFilePath = Preferences.api_key_file; - if (keyFilePath != null && !keyFilePath.trim().isEmpty() && !keyFilePath.startsWith("$(")) { - try { - Path path = Paths.get(keyFilePath.trim()); - if (Files.exists(path)) { - apiKey = Files.readString(path).trim(); - logger.log(Level.FINE, "Using API key from file: " + keyFilePath); - return apiKey; - } else { - logger.log(Level.WARNING, "API key file not found: " + keyFilePath); - } - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to read API key from file: " + keyFilePath, e); - } - } - - logger.log(Level.WARNING, "No API key configured. Set QSERVER_HTTP_SERVER_API_KEY environment variable " + - "or QSERVER_HTTP_SERVER_API_KEYFILE to point to a file containing the API key."); - return null; - } - - @Override public AppInstance create(java.net.URI resource) { - return ApplicationService.createInstance(NAME); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerInstance.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerInstance.java deleted file mode 100644 index b522eea5c1..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerInstance.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.phoebus.applications.queueserver; - -import javafx.scene.Node; -import org.phoebus.framework.persistence.Memento; -import org.phoebus.framework.spi.AppDescriptor; -import org.phoebus.framework.spi.AppInstance; - -final class QueueServerInstance implements AppInstance { - - private final AppDescriptor desc; - private final Node view; - - QueueServerInstance(AppDescriptor desc, Node view) { - this.desc = desc; - this.view = view; - } - - @Override public AppDescriptor getAppDescriptor() { return desc; } - public Node create() { return view; } - - @Override public void restore(Memento m) { /* nothing */ } - @Override public void save (Memento m) { /* nothing */ } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerMenuEntry.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerMenuEntry.java deleted file mode 100644 index 06f99e5266..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerMenuEntry.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.phoebus.applications.queueserver; - -import org.phoebus.framework.workbench.ApplicationService; -import org.phoebus.ui.javafx.ImageCache; -import org.phoebus.ui.spi.MenuEntry; -import javafx.scene.image.Image; - -public final class QueueServerMenuEntry implements MenuEntry { - - @Override public String getName() { return Messages.QueueServer; } - @Override public Image getIcon() { return ImageCache.getImage( - QueueServerApp.class, - "/icons/bluesky.png"); } // same icon as descriptor - @Override public String getMenuPath() { return Messages.QueueServerMenuPath; } - - @Override public Void call() throws Exception { - ApplicationService.createInstance(QueueServerApp.NAME); - return null; - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputText.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputText.java deleted file mode 100644 index e560588ad4..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputText.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -public record ConsoleOutputText( - boolean success, - String msg, - String text -) {} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUid.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUid.java deleted file mode 100644 index 642e074370..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUid.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record ConsoleOutputUid( - boolean success, - String msg, - @JsonProperty("console_output_uid") - String uid -) {} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUpdate.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUpdate.java deleted file mode 100644 index 9b441f6b03..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputUpdate.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -import java.util.List; -import java.util.Map; - -public record ConsoleOutputUpdate( - boolean success, - String msg, - @JsonProperty("console_output_msgs") - List> consoleOutputMsgs, - @JsonProperty("last_msg_uid") - String lastMsgUid -) {} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputWsMessage.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputWsMessage.java deleted file mode 100644 index 80c8c15206..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/ConsoleOutputWsMessage.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * WebSocket message from /console_output/ws endpoint. - * Format: {"time": timestamp, "msg": text} - */ -public record ConsoleOutputWsMessage( - @JsonProperty("time") double time, - @JsonProperty("msg") String msg -) { - /** - * Get timestamp as milliseconds since epoch. - */ - public long timestampMillis() { - return (long) (time * 1000); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/Envelope.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/Envelope.java deleted file mode 100644 index 0d0a6c2bf0..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/Envelope.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** Generic wrapper for virtually every Bluesky HTTP response. */ -public record Envelope( - boolean success, - @JsonProperty("msg") String msg, - @JsonAlias("detail") String detail, - T payload // may be null -) {} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/EverythingElse.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/EverythingElse.java deleted file mode 100644 index b46457f0cb..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/EverythingElse.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import java.util.Map; - -/** - * Marker types for endpoints whose payload is either - * • empty, or - * • an arbitrary JSON object we don’t inspect yet. - * - */ -public interface EverythingElse { - /** Server returns only {success,msg} */ - enum Empty implements EverythingElse { INSTANCE } - - /** Server returns some JSON object we keep as Map */ - record Arbitrary(Map value) implements EverythingElse {} -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/HistoryGetPayload.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/HistoryGetPayload.java deleted file mode 100644 index 58027efd76..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/HistoryGetPayload.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; - -public record HistoryGetPayload( - @JsonProperty("items") List items, - @JsonProperty("plan_history_uid") String planHistoryUid -) {} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/NoBody.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/NoBody.java deleted file mode 100644 index 945ea8c180..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/NoBody.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -/** Marker for requests that send **no** JSON body. */ -public enum NoBody { INSTANCE } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueGetPayload.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueGetPayload.java deleted file mode 100644 index 5efb946760..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueGetPayload.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; - -public record QueueGetPayload( - - @JsonProperty("items") - List queue, - - @JsonProperty("running_item") QueueItem runningItem, - @JsonProperty("plan_queue_uid") String planQueueUid, - @JsonProperty("running_item_uid") String runningItemUid -) {} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItem.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItem.java deleted file mode 100644 index 4a1760fa5d..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItem.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import java.util.Map; - -/** Plan or instruction that can sit in the queue/history. */ -public record QueueItem( - @JsonProperty("item_type") String itemType, // "plan" | "instruction" - String name, // "count", "sleep", ... - List args, - Map kwargs, - @JsonProperty("item_uid") String itemUid, - String user, - @JsonProperty("user_group") String userGroup, - Map result -) {} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAdd.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAdd.java deleted file mode 100644 index 2efca787ac..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAdd.java +++ /dev/null @@ -1,37 +0,0 @@ -// src/main/java/com/jbi/api/AddQueueItemRequest.java -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import java.util.Map; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record QueueItemAdd( - @JsonProperty("item") Item item, - String user, - @JsonProperty("user_group") String userGroup, - @JsonProperty("after_uid") String afterUid) { - - /** Convenience constructor – adds to end of queue. */ - public QueueItemAdd(Item item, String user, String userGroup) { - this(item, user, userGroup, null); - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record Item( - @JsonProperty("item_type") String itemType, - String name, - List args, - Map kwargs - ) { - - public static Item from(QueueItem qi) { - return new Item( - qi.itemType(), - qi.name(), - qi.args(), - qi.kwargs()); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAddBatch.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAddBatch.java deleted file mode 100644 index dfbb9d6113..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemAddBatch.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record QueueItemAddBatch( - - @JsonProperty("items") List items, - @JsonProperty("user") String user, - @JsonProperty("user_group") String userGroup, - @JsonProperty("after_uid") String afterUid -) { - /** Convenience constructor – adds to end of queue. */ - public QueueItemAddBatch(List items, String user, String userGroup) { - this(items, user, userGroup, null); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMove.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMove.java deleted file mode 100644 index 65ff363917..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMove.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record QueueItemMove( - String uid, - @JsonProperty("before_uid") String beforeUid, - @JsonProperty("after_uid") String afterUid) { - - public static QueueItemMove before(String uid, String ref) { - return new QueueItemMove(uid, ref, null); - } - public static QueueItemMove after(String uid, String ref) { - return new QueueItemMove(uid, null, ref); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMoveBatch.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMoveBatch.java deleted file mode 100644 index ee61ba3209..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/QueueItemMoveBatch.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record QueueItemMoveBatch( - List uids, - @JsonProperty("before_uid") String beforeUid, - @JsonProperty("after_uid") String afterUid) { - - public static QueueItemMoveBatch before(List uids, String ref) { - return new QueueItemMoveBatch(uids, ref, null); - } - public static QueueItemMoveBatch after(List uids, String ref) { - return new QueueItemMoveBatch(uids, null, ref); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusResponse.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusResponse.java deleted file mode 100644 index 656c3b5f80..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record StatusResponse( - - String msg, - - @JsonProperty("items_in_queue") int itemsInQueue, - @JsonProperty("items_in_history") int itemsInHistory, - @JsonProperty("running_item_uid") String runningItemUid, - @JsonProperty("manager_state") String managerState, - @JsonProperty("queue_stop_pending") boolean queueStopPending, - @JsonProperty("queue_autostart_enabled") boolean queueAutostartEnabled, - @JsonProperty("worker_environment_exists") boolean workerEnvironmentExists, - @JsonProperty("worker_environment_state") String workerEnvironmentState, - @JsonProperty("worker_background_tasks") int workerBackgroundTasks, - @JsonProperty("re_state") String reState, - @JsonProperty("ip_kernel_state") String ipKernelState, - @JsonProperty("ip_kernel_captured") Boolean ipKernelCaptured, - @JsonProperty("pause_pending") boolean pausePending, - @JsonProperty("run_list_uid") String runListUid, - @JsonProperty("plan_queue_uid") String planQueueUid, - @JsonProperty("plan_history_uid") String planHistoryUid, - @JsonProperty("devices_existing_uid") String devicesExistingUid, - @JsonProperty("plans_existing_uid") String plansExistingUid, - @JsonProperty("devices_allowed_uid") String devicesAllowedUid, - @JsonProperty("plans_allowed_uid") String plansAllowedUid, - @JsonProperty("task_results_uid") String taskResultsUid, - @JsonProperty("lock_info_uid") String lockInfoUid, - @JsonProperty("plan_queue_mode") PlanQueueMode planQueueMode, - @JsonProperty("lock") LockInfo lock -) -{ - public record LockInfo( - boolean environment, - boolean queue) {} - - public record PlanQueueMode( - boolean loop, - @JsonProperty("ignore_failures") boolean ignoreFailures) {} -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusWsMessage.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusWsMessage.java deleted file mode 100644 index 1e22d0e8a3..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/StatusWsMessage.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Map; - -/** - * WebSocket message from /status/ws endpoint. - * Format: {"time": timestamp, "msg": {"status": {...}}} - */ -public record StatusWsMessage( - @JsonProperty("time") double time, - @JsonProperty("msg") Map msg -) { - /** - * Get timestamp as milliseconds since epoch. - */ - public long timestampMillis() { - return (long) (time * 1000); - } - - /** - * Get the status payload from the message. - * Returns null if not present. - */ - @SuppressWarnings("unchecked") - public Map status() { - if (msg == null) return null; - return (Map) msg.get("status"); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/SystemInfoWsMessage.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/SystemInfoWsMessage.java deleted file mode 100644 index 7efe0bda63..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/api/SystemInfoWsMessage.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.phoebus.applications.queueserver.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Map; - -/** - * WebSocket message from /info/ws endpoint. - * Format: {"time": timestamp, "msg": {msg-class: msg-content}} - * Currently includes status messages and may include additional system info in the future. - */ -public record SystemInfoWsMessage( - @JsonProperty("time") double time, - @JsonProperty("msg") Map msg -) { - /** - * Get timestamp as milliseconds since epoch. - */ - public long timestampMillis() { - return (long) (time * 1000); - } - - /** - * Get the status payload if this is a status message. - * Returns null if not a status message. - */ - @SuppressWarnings("unchecked") - public Map status() { - if (msg == null) return null; - return (Map) msg.get("status"); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/ApiEndpoint.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/ApiEndpoint.java deleted file mode 100644 index fbfa35d451..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/ApiEndpoint.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.phoebus.applications.queueserver.client; - -import static org.phoebus.applications.queueserver.client.HttpMethod.*; - -public enum ApiEndpoint { - - PING (GET , "/api/ping"), - STATUS (GET , "/api/status"), - CONFIG_GET (GET , "/api/config/get"), - QUEUE_START (POST, "/api/queue/start"), - QUEUE_STOP (POST, "/api/queue/stop"), - QUEUE_STOP_CANCEL (POST, "/api/queue/stop/cancel"), - QUEUE_GET (GET , "/api/queue/get"), - QUEUE_CLEAR (POST, "/api/queue/clear"), - QUEUE_AUTOSTART (POST, "/api/queue/autostart"), - QUEUE_MODE_SET (POST, "/api/queue/mode/set"), - QUEUE_ITEM_ADD (POST, "/api/queue/item/add"), - QUEUE_ITEM_ADD_BATCH(POST,"/api/queue/item/add/batch"), - QUEUE_ITEM_GET (GET , "/api/queue/item/get"), - QUEUE_ITEM_UPDATE (POST, "/api/queue/item/update"), - QUEUE_ITEM_REMOVE (POST, "/api/queue/item/remove"), - QUEUE_ITEM_REMOVE_BATCH(POST,"/api/queue/item/remove/batch"), - QUEUE_ITEM_MOVE (POST, "/api/queue/item/move"), - QUEUE_ITEM_MOVE_BATCH(POST,"/api/queue/item/move/batch"), - QUEUE_ITEM_EXECUTE(POST, "/api/queue/item/execute"), - HISTORY_GET (GET , "/api/history/get"), - HISTORY_CLEAR (POST, "/api/history/clear"), - STREAM_CONSOLE_OUTPUT (GET , "/api/stream_console_output"), - CONSOLE_OUTPUT (GET , "/api/console_output"), - CONSOLE_OUTPUT_UID (GET , "/api/console_output/uid"), - CONSOLE_OUTPUT_UPDATE (GET , "/api/console_output_update"), - ENVIRONMENT_OPEN (POST, "/api/environment/open"), - ENVIRONMENT_CLOSE (POST, "/api/environment/close"), - ENVIRONMENT_DESTROY(POST,"/api/environment/destroy"), - ENVIRONMENT_UPDATE(POST, "/api/environment/update"), - RE_PAUSE (POST, "/api/re/pause"), - RE_RESUME (POST, "/api/re/resume"), - RE_STOP (POST, "/api/re/stop"), - RE_ABORT (POST, "/api/re/abort"), - RE_HALT (POST, "/api/re/halt"), - RE_RUNS (POST, "/api/re/runs"), - PLANS_ALLOWED (GET , "/api/plans/allowed"), - DEVICES_ALLOWED (GET , "/api/devices/allowed"), - PLANS_EXISTING (GET , "/api/plans/existing"), - DEVICES_EXISTING (GET , "/api/devices/existing"), - PERMISSIONS_RELOAD(POST, "/api/permissions/reload"), - PERMISSIONS_GET (GET , "/api/permissions/get"), - PERMISSIONS_SET (POST, "/api/permissions/set"), - SCRIPT_UPLOAD (POST, "/api/script/upload"), - FUNCTION_EXECUTE (POST, "/api/function/execute"), - TASK_STATUS (GET , "/api/task/status"), - TASK_RESULT (GET , "/api/task/result"), - LOCK (POST, "/api/lock"), - UNLOCK (POST, "/api/unlock"), - LOCK_INFO (GET , "/api/lock/info"), - KERNEL_INTERRUPT (POST, "/api/kernel/interrupt"), - MANAGER_STOP (POST, "/api/manager/stop"), - MANAGER_KILL (POST, "/api/test/manager/kill"), - // --- Auth --- - SESSION_REFRESH (POST, "/api/auth/session/refresh"), - APIKEY_NEW (POST, "/api/auth/apikey"), - APIKEY_INFO (GET , "/api/auth/apikey"), - APIKEY_DELETE (DELETE,"/api/auth/apikey"), - WHOAMI (GET , "/api/auth/whoami"), - API_SCOPES (GET , "/api/auth/scopes"), - LOGOUT (POST, "/api/auth/logout"); - - private final Endpoint endpoint; - ApiEndpoint(HttpMethod method, String path) { - this.endpoint = new Endpoint(method, path); - } - public Endpoint endpoint() { return endpoint; } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/Endpoint.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/Endpoint.java deleted file mode 100644 index e9c2c850fb..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/Endpoint.java +++ /dev/null @@ -1,3 +0,0 @@ -package org.phoebus.applications.queueserver.client; - -public record Endpoint(HttpMethod method, String path) {} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/HttpMethod.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/HttpMethod.java deleted file mode 100644 index 8882974abe..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/HttpMethod.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.phoebus.applications.queueserver.client; - -public enum HttpMethod { GET, POST, DELETE, PUT } - diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/QueueServerWebSocket.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/QueueServerWebSocket.java deleted file mode 100644 index ec40457c90..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/QueueServerWebSocket.java +++ /dev/null @@ -1,200 +0,0 @@ -package org.phoebus.applications.queueserver.client; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Generic WebSocket client for Queue Server streaming endpoints. - * Connects to a WebSocket, parses JSON messages to the specified type, and notifies listeners. - * - * @param the message type (e.g., ConsoleOutputWsMessage, StatusWsMessage) - */ -public final class QueueServerWebSocket implements AutoCloseable { - - private static final Logger logger = Logger.getLogger(QueueServerWebSocket.class.getPackageName()); - private static final ObjectMapper JSON = new ObjectMapper(); - - private final String wsUrl; - private final String authHeader; - private final Class messageType; - private final List> listeners = new CopyOnWriteArrayList<>(); - private final AtomicBoolean active = new AtomicBoolean(false); - - private WebSocket webSocket; - private final StringBuilder messageBuffer = new StringBuilder(); - - /** - * Create a new WebSocket client. - * - * @param wsUrl the WebSocket URL (e.g., "ws://localhost:60610/api/console_output/ws") - * @param apiKey the API key for authentication - * @param messageType the class of the message type to parse - */ - public QueueServerWebSocket(String wsUrl, String apiKey, Class messageType) { - this.wsUrl = wsUrl; - this.authHeader = "ApiKey " + apiKey; - this.messageType = messageType; - } - - /** - * Add a listener that will be called when messages are received. - * - * @param listener the listener to add - */ - public void addListener(Consumer listener) { - listeners.add(listener); - } - - /** - * Remove a listener. - * - * @param listener the listener to remove - */ - public void removeListener(Consumer listener) { - listeners.remove(listener); - } - - /** - * Connect to the WebSocket and start receiving messages. - */ - public void connect() { - if (active.getAndSet(true)) { - logger.log(Level.WARNING, "WebSocket already connected: " + wsUrl); - return; - } - - logger.log(Level.FINE, "Connecting to WebSocket: " + wsUrl); - - HttpClient client = HttpClient.newHttpClient(); - CompletableFuture wsFuture = client.newWebSocketBuilder() - .header("Authorization", authHeader) - .buildAsync(URI.create(wsUrl), new WebSocketListener()); - - wsFuture.whenComplete((ws, ex) -> { - if (ex != null) { - logger.log(Level.FINE, "Failed to connect to WebSocket: " + wsUrl + " (" + ex.getMessage() + ")"); - active.set(false); - } else { - this.webSocket = ws; - logger.log(Level.FINE, "WebSocket connected: " + wsUrl); - } - }); - } - - /** - * Disconnect from the WebSocket. - */ - public void disconnect() { - if (!active.getAndSet(false)) { - return; - } - - logger.log(Level.FINE, "Disconnecting from WebSocket: " + wsUrl); - - if (webSocket != null) { - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Client closing"); - webSocket = null; - } - } - - /** - * Check if the WebSocket is currently connected. - */ - public boolean isConnected() { - return active.get() && webSocket != null && !webSocket.isInputClosed(); - } - - @Override - public void close() { - disconnect(); - } - - /** - * WebSocket listener implementation. - */ - private class WebSocketListener implements WebSocket.Listener { - - @Override - public void onOpen(WebSocket webSocket) { - logger.log(Level.FINE, "WebSocket opened: " + wsUrl); - webSocket.request(1); - } - - @Override - public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { - messageBuffer.append(data); - - if (last) { - String fullMessage = messageBuffer.toString(); - messageBuffer.setLength(0); - - try { - T message = JSON.readValue(fullMessage, messageType); - notifyListeners(message); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to parse WebSocket message: " + fullMessage, e); - } - } - - webSocket.request(1); - return null; - } - - @Override - public CompletionStage onBinary(WebSocket webSocket, ByteBuffer data, boolean last) { - // Not expected for Queue Server WebSockets - webSocket.request(1); - return null; - } - - @Override - public CompletionStage onPing(WebSocket webSocket, ByteBuffer message) { - webSocket.request(1); - return null; - } - - @Override - public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - webSocket.request(1); - return null; - } - - @Override - public CompletionStage onClose(WebSocket webSocket, int statusCode, String reason) { - logger.log(Level.FINE, "WebSocket closed: " + wsUrl + " (" + statusCode + ": " + reason + ")"); - active.set(false); - return null; - } - - @Override - public void onError(WebSocket webSocket, Throwable error) { - logger.log(Level.SEVERE, "WebSocket error: " + wsUrl, error); - active.set(false); - } - } - - /** - * Notify all listeners with the received message. - */ - private void notifyListeners(T message) { - for (Consumer listener : listeners) { - try { - listener.accept(message); - } catch (Exception e) { - logger.log(Level.SEVERE, "Error in WebSocket listener", e); - } - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java deleted file mode 100644 index 45871e5166..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java +++ /dev/null @@ -1,193 +0,0 @@ -package org.phoebus.applications.queueserver.client; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.applications.queueserver.api.*; -import org.phoebus.applications.queueserver.api.Envelope; -import org.phoebus.applications.queueserver.api.EverythingElse.Arbitrary; -import org.phoebus.applications.queueserver.api.NoBody; -import org.phoebus.applications.queueserver.util.HttpSupport; -import org.phoebus.applications.queueserver.util.RateLimiter; - -import java.net.URI; -import java.net.http.*; -import java.time.Duration; -import java.util.Map; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class RunEngineHttpClient { - - private static volatile RunEngineHttpClient INSTANCE; - - // Default 10 req/sec - public static void initialize(String baseUrl, String apiKey) { - initialize(baseUrl, apiKey, 10.0); - } - public static void initialize(String baseUrl, String apiKey, double permitsPerSecond) { - if (INSTANCE == null) { - synchronized (RunEngineHttpClient.class) { - if (INSTANCE == null) - INSTANCE = new RunEngineHttpClient(baseUrl, apiKey, permitsPerSecond); - } - } - } - public static RunEngineHttpClient get() { - if (INSTANCE == null) throw new IllegalStateException("RunEngineHttpClient not initialised"); - return INSTANCE; - } - - private final HttpClient http; - private final ObjectMapper mapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .setSerializationInclusion(JsonInclude.Include.NON_NULL);; - private final String base; - private final String apiKey; - private final RateLimiter limiter; - private static final Logger logger = HttpSupport.logger; - - private RunEngineHttpClient(String baseUrl, String apiKey, double permitsPerSecond) { - this.http = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .connectTimeout(Duration.ofSeconds(5)) - .build(); - this.base = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; - this.apiKey = apiKey; - this.limiter = new RateLimiter(permitsPerSecond); - } - - // getters - public String getBaseUrl() { return base; } - public String getApiKey() { return apiKey; } - public HttpClient httpClient() { return http; } - - public Envelope call(ApiEndpoint ep, Object body) throws Exception { - Object requestBody = (body == NoBody.INSTANCE) ? null : body; - return send(ep, requestBody, new TypeReference>() {}); - } - - // raw JSON (Map) for CLI / REPL - public Map send(ApiEndpoint api, Object body) throws Exception { - return send(api, body, new TypeReference>() {}); - } - - public T send(ApiEndpoint api, Object body, Class type) throws Exception { - HttpRequest req = build(api.endpoint(), body); - return executeWithRetry(req, api, rsp -> - { - try { - return (type == Void.class) ? null : mapper.readValue(rsp.body(), type); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - } - - public T send(ApiEndpoint api, Object body, TypeReference ref) throws Exception { - HttpRequest req = build(api.endpoint(), body); - return executeWithRetry(req, api, rsp -> { - try { return mapper.readValue(rsp.body(), ref); } - catch (Exception e) { throw new RuntimeException(e); } - }); - } - - T send(ApiEndpoint api, Object body, Class type, String extraQuery) throws Exception { - Endpoint ep = api.endpoint(); - String path = ep.path() + (extraQuery == null ? "" : extraQuery); - HttpRequest req = build(new Endpoint(ep.method(), path), body); - return executeWithRetry(req, api, rsp -> { - try { - return mapper.readValue(rsp.body(), type); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - } - - private T executeWithRetry(HttpRequest req, ApiEndpoint ep, - Function, T> reader) throws Exception { - int attempt = 0; - long back = HttpSupport.INITIAL_BACKOFF_MS; - - while (true) { - attempt++; - limiter.acquire(); - long t0 = System.nanoTime(); - try { - logger.log(Level.FINEST, ep + " attempt " + attempt); - HttpResponse rsp = http.send(req, HttpResponse.BodyHandlers.ofString()); - logger.log(Level.FINEST, ep + " " + rsp.statusCode() + " " + HttpSupport.elapsed(t0) + " ms"); - check(rsp, ep); - return reader.apply(rsp); - } catch (java.io.IOException ex) { - if (!HttpSupport.isRetryable(req) || attempt >= HttpSupport.MAX_RETRIES) throw ex; - logger.log(Level.FINE, ep + " transport error (" + ex.getClass().getSimpleName() + - "), retry in " + back + " ms (attempt " + attempt + ")"); - Thread.sleep(back); - back = Math.round(back * HttpSupport.BACKOFF_MULTIPLIER); - } - } - } - - private HttpRequest build(Endpoint ep, Object body) throws Exception { - HttpRequest.Builder b = HttpRequest.newBuilder() - .uri(URI.create(base + ep.path())) - .header("Authorization", "ApiKey " + apiKey) - .header("Content-Type", "application/json"); - - return switch (ep.method()) { - case GET -> b.GET().build(); - case DELETE -> b.DELETE().build(); - case POST, PUT -> b.method(ep.method().name(), - (body == null) - ? HttpRequest.BodyPublishers.noBody() - : HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body))) - .build(); - }; - } - - private void check(HttpResponse rsp, ApiEndpoint api) throws Exception { - if (rsp.statusCode() >= 200 && rsp.statusCode() < 300) { - try { - Map map = mapper.readValue( - rsp.body(), new TypeReference<>() {}); - Object s = map.get("success"); - if (Boolean.FALSE.equals(s)) { - String msg = String.valueOf(map.getOrDefault("msg", "(no msg)")); - throw new RequestFailedException(api, msg); - } - } catch (JsonProcessingException ignore) { - /* response isn't a generic object, that's fine (e.g. plain "OK") */ - } - } else if (rsp.statusCode() < 500) { - throw new ClientErrorException(api, rsp); - } else { - throw new ServerErrorException(api, rsp); - } - } - - - public sealed static class BlueskyException extends RuntimeException - permits ClientErrorException, ServerErrorException, RequestFailedException { - BlueskyException(String m) { super(m); } - } - public static final class ClientErrorException extends BlueskyException { - ClientErrorException(ApiEndpoint api, HttpResponse rsp) { - super(api + " → " + rsp.statusCode() + " " + rsp.body()); - } - } - public static final class ServerErrorException extends BlueskyException { - ServerErrorException(ApiEndpoint api, HttpResponse rsp) { - super(api + " → server error " + rsp.statusCode()); - } - } - public static final class RequestFailedException extends BlueskyException { - RequestFailedException(ApiEndpoint api, String msg) { - super(api + " → " + msg); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java deleted file mode 100644 index bc5252115d..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java +++ /dev/null @@ -1,361 +0,0 @@ -package org.phoebus.applications.queueserver.client; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.applications.queueserver.api.*; -import org.phoebus.applications.queueserver.api.*; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Exhaustive façade: one method per QueueServer REST endpoint. - * - *

Each method blocks, calls {@link RunEngineHttpClient#call(ApiEndpoint, Object)}, - * and returns the raw {@link Envelope}. Pass {@code null} or {@link NoBody#INSTANCE} - * when no JSON request body is needed. The envelope’s {@code payload()} is - * usually a {@code Map<String,Object>} (typed DTOs exist only for STATUS - * and QUEUE_GET).

- * - *

All errors propagate as {@link RunEngineHttpClient.BlueskyException} or - * {@link java.io.IOException} / {@link java.net.http.HttpTimeoutException}.

- */ -public final class RunEngineService { - - private final RunEngineHttpClient http = RunEngineHttpClient.get(); - private static final Logger logger = Logger.getLogger(RunEngineService.class.getPackageName()); - - /* ---- Ping & status --------------------------------------------------- */ - - public Envelope ping() throws Exception { return http.call(ApiEndpoint.PING, NoBody.INSTANCE); } - public StatusResponse status() throws Exception { - logger.log(Level.FINEST, "Fetching status"); - return http.send(ApiEndpoint.STATUS, NoBody.INSTANCE, StatusResponse.class); - } - public Envelope configGet() throws Exception { return http.call(ApiEndpoint.CONFIG_GET, NoBody.INSTANCE); } - - /* ───────── Queue – typed helpers ───────── */ - - public QueueGetPayload queueGetTyped() throws Exception { - return http.send(ApiEndpoint.QUEUE_GET, NoBody.INSTANCE, QueueGetPayload.class); - } - - public HistoryGetPayload historyGetTyped() throws Exception { - return http.send(ApiEndpoint.HISTORY_GET, NoBody.INSTANCE, HistoryGetPayload.class); - } - - /* ---- Queue control --------------------------------------------------- */ - - public Envelope queueStart() throws Exception { return http.call(ApiEndpoint.QUEUE_START, NoBody.INSTANCE); } - public Envelope queueStop() throws Exception { return http.call(ApiEndpoint.QUEUE_STOP, NoBody.INSTANCE); } - public Envelope queueStopCancel() throws Exception { return http.call(ApiEndpoint.QUEUE_STOP_CANCEL,NoBody.INSTANCE); } - public Envelope queueGet() throws Exception { return http.call(ApiEndpoint.QUEUE_GET, NoBody.INSTANCE); } - public Envelope queueClear() throws Exception { return http.call(ApiEndpoint.QUEUE_CLEAR, NoBody.INSTANCE); } - public Envelope queueAutostart(Object body) throws Exception { return http.call(ApiEndpoint.QUEUE_AUTOSTART, body); } - public Envelope queueAutostart(boolean enable) throws Exception { return queueAutostart(Map.of("enable", enable)); } - public Envelope queueModeSet(Object body) throws Exception { return http.call(ApiEndpoint.QUEUE_MODE_SET, body); } - - /* ---- single-item add ------------------------------------------------ */ - - public Envelope queueItemAdd(Object body) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_ADD, body); } - - public Envelope queueItemAdd(QueueItem item, - String user, - String group, - String afterUid) throws Exception { - - QueueItemAdd req = new QueueItemAdd(QueueItemAdd.Item.from(item), user, group, afterUid); - return queueItemAdd(req); - } - - public Envelope queueItemAdd(QueueItem item, - String user, - String group) throws Exception { - return queueItemAdd(item, user, group, null); - } - - public Envelope queueItemAdd(QueueItem item) throws Exception { - return queueItemAdd(item, "GUI Client", "primary", null); - } - - public Envelope queueItemAdd(QueueItem item, String afterUid) throws Exception { - return queueItemAdd(item, "GUI Client", "primary", afterUid); - } - - /* ---- move helpers --------------------------------------------------- */ - - public void moveSingle(QueueItemMove dto) throws Exception { - http.call(ApiEndpoint.QUEUE_ITEM_MOVE, dto); - } - public void moveSingle(String uid, String ref, boolean before) throws Exception { - moveSingle(before ? QueueItemMove.before(uid, ref) - : QueueItemMove.after (uid, ref)); - } - - public void moveBatch(QueueItemMoveBatch dto) throws Exception { - http.call(ApiEndpoint.QUEUE_ITEM_MOVE_BATCH, dto); - } - public void moveBatch(List uids, String ref, boolean before) throws Exception { - moveBatch(before ? QueueItemMoveBatch.before(uids, ref) - : QueueItemMoveBatch.after (uids, ref)); - } - - /* ---- UID-returning add helpers -------------------------------------- */ - - /** Add a single item and return the server-assigned UID. */ - @SuppressWarnings("unchecked") - public String addItemGetUid(Object body) throws Exception { - Map response = http.send(ApiEndpoint.QUEUE_ITEM_ADD, body); - if (Boolean.TRUE.equals(response.get("success"))) { - Map item = (Map) response.get("item"); - if (item != null) return (String) item.get("item_uid"); - } - throw new RuntimeException("Failed to add item: " + response.get("msg")); - } - - /** Add a batch of items and return the server-assigned UIDs. */ - @SuppressWarnings("unchecked") - public List addBatchGetUids(Object body) throws Exception { - Map response = http.send(ApiEndpoint.QUEUE_ITEM_ADD_BATCH, body); - if (Boolean.TRUE.equals(response.get("success"))) { - List> items = (List>) response.get("items"); - if (items != null) { - return items.stream() - .map(i -> (String) i.get("item_uid")) - .filter(java.util.Objects::nonNull) - .toList(); - } - } - throw new RuntimeException("Failed to add batch: " + response.get("msg")); - } - - /* ---- duplicate helper ---------------------------------------------- */ - - public void addAfter(QueueItem item, String afterUid) throws Exception { - QueueItemAdd req = new QueueItemAdd(QueueItemAdd.Item.from(item), - "GUI Client", "primary", afterUid); - queueItemAdd(req); - } - - /* ---- batch-add helpers --------------------------------------------- */ - - public Envelope queueItemAddBatch(Object body) throws Exception { - return http.call(ApiEndpoint.QUEUE_ITEM_ADD_BATCH, body); - } - - public Envelope queueItemAddBatch(QueueItemAddBatch req) throws Exception { - return http.call(ApiEndpoint.QUEUE_ITEM_ADD_BATCH, req); - } - - public Envelope queueItemAddBatch(List items, - String user, - String group) throws Exception { - return queueItemAddBatch(new QueueItemAddBatch(items, user, group)); - } - - public Envelope queueItemAddBatch(QueueItemMoveBatch body ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_ADD_BATCH, body); } - public Envelope queueItemGet(Object params ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_GET, params); } - public Envelope queueItemUpdate(Object b ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_UPDATE, b); } - - public Envelope queueItemUpdate(QueueItem item) throws Exception { - Map updateRequest = Map.of( - "item", Map.of( - "item_type", item.itemType(), - "name", item.name(), - "args", item.args(), - "kwargs", item.kwargs(), - "item_uid", item.itemUid() - ), - "user", item.user() != null ? item.user() : "GUI Client", - "user_group", item.userGroup() != null ? item.userGroup() : "primary", - "replace", true - ); - return queueItemUpdate(updateRequest); - } - public Envelope queueItemRemove(Object b ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_REMOVE, b); } - public Envelope queueItemRemoveBatch(Object b) throws Exception {return http.call(ApiEndpoint.QUEUE_ITEM_REMOVE_BATCH,b); } - public Envelope queueItemMove(Object b ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_MOVE, b); } - public Envelope queueItemMove(QueueItemMove body) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_MOVE, body); } - public Envelope queueItemMoveBatch(Object b) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_MOVE_BATCH,b);} - public Envelope queueItemExecute(Object b ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_EXECUTE,b); } - - /* ---- History --------------------------------------------------------- */ - - public Envelope historyGet() throws Exception { return http.call(ApiEndpoint.HISTORY_GET, NoBody.INSTANCE); } - public Envelope historyClear() throws Exception { return http.call(ApiEndpoint.HISTORY_CLEAR, NoBody.INSTANCE); } - - - /* ───────── Console monitor ───────── */ - - public InputStream streamConsoleOutput() throws Exception { - logger.log(Level.FINE, "Opening console output stream"); - HttpRequest req = HttpRequest.newBuilder() - .uri(URI.create(http.getBaseUrl() + ApiEndpoint.STREAM_CONSOLE_OUTPUT.endpoint().path())) - .header("Authorization", "ApiKey " + http.getApiKey()) - .GET() - .build(); - // no retry/limiting – you open it once and keep reading - HttpResponse rsp = - http.httpClient().send(req, HttpResponse.BodyHandlers.ofInputStream()); - if (rsp.statusCode() < 200 || rsp.statusCode() >= 300) { - logger.log(Level.WARNING, "Console stream failed with HTTP " + rsp.statusCode()); - throw new IOException("console stream - HTTP " + rsp.statusCode()); - } - logger.log(Level.FINE, "Console output stream opened successfully"); - return rsp.body(); - } - - public ConsoleOutputText consoleOutput(int nLines) throws Exception { - String q = "?nlines=" + nLines; - return http.send(ApiEndpoint.CONSOLE_OUTPUT, null, - ConsoleOutputText.class, q); - } - - public ConsoleOutputUid consoleOutputUid() throws Exception { - return http.send(ApiEndpoint.CONSOLE_OUTPUT_UID, null, ConsoleOutputUid.class); - } - - private static final ObjectMapper JSON = new ObjectMapper(); - - - public ConsoleOutputUpdate consoleOutputUpdate(String lastUid) throws Exception { - String bodyJson = JSON.writeValueAsString(Map.of("last_msg_uid", lastUid)); - - HttpRequest req = HttpRequest.newBuilder() - .uri(URI.create( - http.getBaseUrl() + - ApiEndpoint.CONSOLE_OUTPUT_UPDATE - .endpoint() - .path())) - .header("Authorization", "ApiKey " + http.getApiKey()) - .header("Content-Type", "application/json") - .method("GET", HttpRequest.BodyPublishers.ofString(bodyJson)) - .build(); - - HttpResponse rsp = - http.httpClient().send(req, HttpResponse.BodyHandlers.ofString()); - - if (rsp.statusCode() < 200 || rsp.statusCode() >= 300) { - throw new IOException("console_output_update – HTTP " - + rsp.statusCode() + " – " + rsp.body()); - } - - return JSON.readValue(rsp.body(), ConsoleOutputUpdate.class); - } - - - - /* ---- Environment ----------------------------------------------------- */ - - public Envelope environmentOpen() throws Exception { return http.call(ApiEndpoint.ENVIRONMENT_OPEN, NoBody.INSTANCE); } - public Envelope environmentClose() throws Exception { return http.call(ApiEndpoint.ENVIRONMENT_CLOSE, NoBody.INSTANCE); } - public Envelope environmentDestroy() throws Exception { return http.call(ApiEndpoint.ENVIRONMENT_DESTROY,NoBody.INSTANCE); } - public Envelope environmentUpdate(Object b) throws Exception { return http.call(ApiEndpoint.ENVIRONMENT_UPDATE, b); } - - /* ---- Run Engine control --------------------------------------------- */ - - public Envelope rePause() throws Exception { return http.call(ApiEndpoint.RE_PAUSE, NoBody.INSTANCE); } - public Envelope rePause(String option) throws Exception { - if (option == null || option.isBlank()) - option = "deferred"; // QS default - return http.call(ApiEndpoint.RE_PAUSE, Map.of("option", option)); - } - - public Envelope reResume() throws Exception { return http.call(ApiEndpoint.RE_RESUME, NoBody.INSTANCE); } - public Envelope reStop() throws Exception { return http.call(ApiEndpoint.RE_STOP, NoBody.INSTANCE); } - public Envelope reAbort() throws Exception { return http.call(ApiEndpoint.RE_ABORT, NoBody.INSTANCE); } - public Envelope reHalt() throws Exception { return http.call(ApiEndpoint.RE_HALT, NoBody.INSTANCE); } - public Envelope reRuns(Object body) throws Exception { return http.call(ApiEndpoint.RE_RUNS, body); } - - /* ---- Permissions & allowed lists ------------------------------------ */ - - public Envelope plansAllowed() throws Exception { return http.call(ApiEndpoint.PLANS_ALLOWED, NoBody.INSTANCE); } - public Map plansAllowedRaw() throws Exception { - logger.log(Level.FINE, "Fetching plans allowed (raw)"); - return http.send(ApiEndpoint.PLANS_ALLOWED, NoBody.INSTANCE); - } - public Envelope devicesAllowed() throws Exception { return http.call(ApiEndpoint.DEVICES_ALLOWED, NoBody.INSTANCE); } - public Envelope plansExisting() throws Exception { return http.call(ApiEndpoint.PLANS_EXISTING, NoBody.INSTANCE); } - public Envelope devicesExisting() throws Exception { return http.call(ApiEndpoint.DEVICES_EXISTING,NoBody.INSTANCE); } - public Envelope permissionsReload() throws Exception { return http.call(ApiEndpoint.PERMISSIONS_RELOAD,NoBody.INSTANCE); } - public Envelope permissionsGet() throws Exception { return http.call(ApiEndpoint.PERMISSIONS_GET, NoBody.INSTANCE); } - public Envelope permissionsSet(Object body ) throws Exception { return http.call(ApiEndpoint.PERMISSIONS_SET, body); } - - /* ---- Script / function ---------------------------------------------- */ - - public Envelope scriptUpload(Object body ) throws Exception { return http.call(ApiEndpoint.SCRIPT_UPLOAD, body); } - public Envelope functionExecute(Object b ) throws Exception { return http.call(ApiEndpoint.FUNCTION_EXECUTE, b); } - - /* ---- Tasks ----------------------------------------------------------- */ - - public Envelope taskStatus(Object params) throws Exception { return http.call(ApiEndpoint.TASK_STATUS, params); } - public Envelope taskResult(Object params) throws Exception { return http.call(ApiEndpoint.TASK_RESULT, params); } - - /* ---- Locks ----------------------------------------------------------- */ - - public Envelope lock(Object body ) throws Exception { return http.call(ApiEndpoint.LOCK, body); } - public Envelope unlock() throws Exception { return http.call(ApiEndpoint.UNLOCK, NoBody.INSTANCE); } - public Envelope lockInfo() throws Exception { return http.call(ApiEndpoint.LOCK_INFO, NoBody.INSTANCE); } - - /* ---- Kernel / Manager ------------------------------------------------ */ - - public Envelope kernelInterrupt() throws Exception { return http.call(ApiEndpoint.KERNEL_INTERRUPT, Map.of("interrupt_task", true)); } - public Envelope managerStop() throws Exception { return http.call(ApiEndpoint.MANAGER_STOP, NoBody.INSTANCE); } - public Envelope managerKill() throws Exception { return http.call(ApiEndpoint.MANAGER_KILL, NoBody.INSTANCE); } - - /* ---- Auth ------------------------------------------------------------ */ - - public Envelope sessionRefresh(Object b ) throws Exception { return http.call(ApiEndpoint.SESSION_REFRESH, b); } - public Envelope apikeyNew (Object body ) throws Exception { return http.call(ApiEndpoint.APIKEY_NEW, body); } - public Envelope apikeyInfo() throws Exception { return http.call(ApiEndpoint.APIKEY_INFO, NoBody.INSTANCE); } - public Envelope apikeyDelete (Object params) throws Exception { return http.call(ApiEndpoint.APIKEY_DELETE, params); } - public Envelope whoAmI() throws Exception { return http.call(ApiEndpoint.WHOAMI, NoBody.INSTANCE); } - public Envelope apiScopes() throws Exception { return http.call(ApiEndpoint.API_SCOPES, NoBody.INSTANCE); } - public Envelope logout() throws Exception { return http.call(ApiEndpoint.LOGOUT, NoBody.INSTANCE); } - - /* ---- WebSockets ------------------------------------------------------ */ - - /** - * Create a WebSocket connection to the console output stream. - * Messages are streamed in real-time as {"time": timestamp, "msg": text}. - * - * @return a WebSocket client that can be connected and listened to - */ - public QueueServerWebSocket createConsoleOutputWebSocket() { - String wsUrl = http.getBaseUrl().replace("http://", "ws://").replace("https://", "wss://") - + "/api/console_output/ws"; - return new QueueServerWebSocket<>(wsUrl, http.getApiKey(), ConsoleOutputWsMessage.class); - } - - /** - * Create a WebSocket connection to the status stream. - * Status messages are sent each time status is updated at RE Manager or at least once per second. - * Messages are formatted as {"time": timestamp, "msg": {"status": {...}}}. - * - * @return a WebSocket client that can be connected and listened to - */ - public QueueServerWebSocket createStatusWebSocket() { - String wsUrl = http.getBaseUrl().replace("http://", "ws://").replace("https://", "wss://") - + "/api/status/ws"; - return new QueueServerWebSocket<>(wsUrl, http.getApiKey(), StatusWsMessage.class); - } - - /** - * Create a WebSocket connection to the system info stream. - * Info stream includes status messages and potentially other system messages. - * Messages are formatted as {"time": timestamp, "msg": {msg-class: msg-content}}. - * - * @return a WebSocket client that can be connected and listened to - */ - public QueueServerWebSocket createSystemInfoWebSocket() { - String wsUrl = http.getBaseUrl().replace("http://", "ws://").replace("https://", "wss://") - + "/api/info/ws"; - return new QueueServerWebSocket<>(wsUrl, http.getApiKey(), SystemInfoWsMessage.class); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java deleted file mode 100644 index 3c12a82e0b..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.view.ViewFactory; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; -import javafx.scene.control.TableView; - -import java.net.URL; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class ApplicationController implements Initializable { - - @FXML private Tab monitorQueueTab; - @FXML private Tab editAndControlQueueTab; - @FXML private TabPane tabPane; - - private static final Logger logger = Logger.getLogger(ApplicationController.class.getPackageName()); - - @Override public void initialize(URL url, ResourceBundle rb) { - logger.log(Level.FINE, "Initializing ApplicationController"); - monitorQueueTab.setContent(ViewFactory.MONITOR_QUEUE.get()); - editAndControlQueueTab.setContent(ViewFactory.EDIT_AND_CONTROL_QUEUE.get()); - - // Disable focus traversal on all components - disableFocusTraversal(monitorQueueTab.getContent()); - disableFocusTraversal(editAndControlQueueTab.getContent()); - - logger.log(Level.FINE, "ApplicationController initialization complete"); - } - - /** - * Recursively disables focus traversal on all nodes in the scene graph, - * except for TableView which remains focus traversable for arrow key navigation. - */ - private void disableFocusTraversal(Node node) { - if (node == null) return; - - // Allow TableView to remain focus traversable for arrow key navigation - if (!(node instanceof TableView)) { - node.setFocusTraversable(false); - } - - if (node instanceof Parent) { - Parent parent = (Parent) node; - for (Node child : parent.getChildrenUnmodifiable()) { - disableFocusTraversal(child); - } - } - } - -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java deleted file mode 100644 index 8fb4e4342a..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; -import javafx.scene.Parent; -import javafx.scene.layout.AnchorPane; - -import java.io.IOException; -import java.net.URL; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class EditAndControlQueueController implements Initializable { - - @FXML private AnchorPane runningPlanContainer; - @FXML private AnchorPane planQueueContainer; - @FXML private AnchorPane planHistoryContainer; - - private static final Logger logger = Logger.getLogger(EditAndControlQueueController.class.getPackageName()); - - @Override - public void initialize(URL url, ResourceBundle resourceBundle) { - logger.log(Level.FINE, "Initializing EditAndControlQueueController"); - loadInto(runningPlanContainer, "/org/phoebus/applications/queueserver/view/ReRunningPlan.fxml", new ReRunningPlanController(false)); - loadInto(planQueueContainer, "/org/phoebus/applications/queueserver/view/RePlanQueue.fxml", new RePlanQueueController(false)); - loadInto(planHistoryContainer, "/org/phoebus/applications/queueserver/view/RePlanHistory.fxml", new RePlanHistoryController(false)); - } - - private void loadInto(AnchorPane container, String fxml, Object controller) { - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml)); - loader.setController(controller); - Parent view = loader.load(); - - container.getChildren().setAll(view); - AnchorPane.setTopAnchor(view, 0.0); - AnchorPane.setBottomAnchor(view, 0.0); - AnchorPane.setLeftAnchor(view, 0.0); - AnchorPane.setRightAnchor(view, 0.0); - } catch (IOException e) { - logger.log(Level.SEVERE, "Failed to load FXML: " + fxml, e); - } - } - -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java deleted file mode 100644 index 45f72f556d..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; -import javafx.scene.Cursor; -import javafx.scene.Parent; -import javafx.scene.control.TitledPane; -import javafx.scene.layout.*; - -import java.io.IOException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class MonitorQueueController implements Initializable { - - @FXML private AnchorPane runningPlanContainer; - @FXML private AnchorPane planQueueContainer; - @FXML private AnchorPane planHistoryContainer; - @FXML private TitledPane runningPane, queuePane, historyPane, consolePane; - @FXML private VBox stack; - - private final Map savedHeights = new HashMap<>(); - private static final Logger logger = Logger.getLogger(MonitorQueueController.class.getPackageName()); - - private static final String BAR_NORMAL = - "-fx-background-color: linear-gradient(to bottom, derive(-fx-base,15%) 0%, derive(-fx-base,-5%) 100%);" + - "-fx-border-color: derive(-fx-base,-25%) transparent derive(-fx-base,-25%) transparent;" + - "-fx-border-width: 1 0 1 0;"; - - @Override - public void initialize(URL url, ResourceBundle resourceBundle) { - logger.log(Level.FINE, "Initializing MonitorQueueController"); - loadInto(runningPlanContainer, "/org/phoebus/applications/queueserver/view/ReRunningPlan.fxml", new ReRunningPlanController(true)); - loadInto(planQueueContainer, "/org/phoebus/applications/queueserver/view/RePlanQueue.fxml", new RePlanQueueController(true)); - loadInto(planHistoryContainer, "/org/phoebus/applications/queueserver/view/RePlanHistory.fxml", new RePlanHistoryController(true)); - - TitledPane[] panes = { runningPane, queuePane, historyPane, consolePane }; - for (TitledPane p : panes) { - p.setMaxHeight(Double.MAX_VALUE); // let VBox stretch it as high as needed - p.setPrefHeight(0); // start with zero so panes share space evenly - p.expandedProperty().addListener((obs, wasExpanded, expanded) -> { - if (expanded) { // RE-EXPANDING - // restore manual height if one was saved, else start shared - double h = savedHeights.getOrDefault(p, 0.0); - p.setPrefHeight(h); - VBox.setVgrow(p, Priority.ALWAYS); - } else { // COLLAPSING - // remember current prefHeight, then shrink to header only - savedHeights.put(p, p.getPrefHeight()); - p.setPrefHeight(Region.USE_COMPUTED_SIZE); - p.setMinHeight(Region.USE_COMPUTED_SIZE); - VBox.setVgrow(p, Priority.NEVER); - } - }); - - VBox.setVgrow(p, p.isExpanded() ? Priority.ALWAYS : Priority.NEVER); - } - - addDragBars(panes); - } - - private void loadInto(AnchorPane container, String fxml, Object controller) { - try { - FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml)); - loader.setController(controller); - Parent view = loader.load(); - - container.getChildren().setAll(view); - AnchorPane.setTopAnchor(view, 0.0); - AnchorPane.setBottomAnchor(view, 0.0); - AnchorPane.setLeftAnchor(view, 0.0); - AnchorPane.setRightAnchor(view, 0.0); - } catch (IOException e) { - logger.log(Level.SEVERE, "Failed to load FXML: " + fxml, e); - } - } - - private void addDragBars(TitledPane[] panes) { - for (int i = 0; i < panes.length - 1; i++) { - - Pane bar = new Pane(); - bar.setStyle(BAR_NORMAL); - bar.setPrefHeight(4); // thin line - bar.setMinHeight(4); - bar.setMaxHeight(4); - VBox.setVgrow(bar, Priority.NEVER); - - final TitledPane upper = panes[i]; - final TitledPane lower = panes[i + 1]; - - bar.setOnMouseEntered(e -> bar.setCursor(Cursor.V_RESIZE)); - - bar.setOnMousePressed(e -> { - double localY = stack.sceneToLocal(e.getSceneX(), e.getSceneY()).getY(); - bar.setUserData(new double[]{ localY }); - }); - - bar.setOnMouseDragged(e -> { - - boolean upOpen = upper.isExpanded(); - boolean lowOpen = lower.isExpanded(); - if (!upOpen && !lowOpen) return; // both collapsed → ignore - - double[] d = (double[]) bar.getUserData(); - double lastY = d[0]; // reference from previous event - double localY = stack.sceneToLocal(e.getSceneX(), e.getSceneY()).getY(); - double dy = localY - lastY; // incremental movement - if (Math.abs(dy) < 0.1) return; // jitter guard - - /* title-bar heights so we never hide headers */ - double upHdr = upper.lookup(".title").getBoundsInParent().getHeight(); - double lowHdr = lower.lookup(".title").getBoundsInParent().getHeight(); - - if (upOpen && lowOpen) { - /* both panes open – resize both sides */ - double newUp = Math.max(upHdr, upper.getPrefHeight() + dy); - double newLow = Math.max(lowHdr, lower.getPrefHeight() - dy); - upper.setPrefHeight(newUp); - lower.setPrefHeight(newLow); - savedHeights.put(upper, newUp); - savedHeights.put(lower, newLow); - - } else if (upOpen) { - /* only upper is open – move it alone */ - double newUp = Math.max(upHdr, upper.getPrefHeight() + dy); - upper.setPrefHeight(newUp); - savedHeights.put(upper, newUp); - - } else { // only lower open - double newLow = Math.max(lowHdr, lower.getPrefHeight() - dy); - lower.setPrefHeight(newLow); - savedHeights.put(lower, newLow); - } - - d[0] = localY; // update reference point - }); - - - - /* insert bar AFTER the upper pane in the VBox children list */ - int insertPos = stack.getChildren().indexOf(upper) + 1; - stack.getChildren().add(insertPos, bar); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java deleted file mode 100644 index 39e94ad6ea..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java +++ /dev/null @@ -1,532 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.applications.queueserver.Preferences; -import org.phoebus.applications.queueserver.api.ConsoleOutputUpdate; -import org.phoebus.applications.queueserver.api.ConsoleOutputWsMessage; -import org.phoebus.applications.queueserver.client.QueueServerWebSocket; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.AppLifecycle; -import org.phoebus.applications.queueserver.util.PollCenter; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.application.Platform; -import javafx.beans.value.ObservableValue; -import javafx.concurrent.Task; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.*; -import javafx.util.StringConverter; -import javafx.util.converter.IntegerStringConverter; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.*; - -public final class ReConsoleMonitorController implements Initializable { - - @FXML private CheckBox autoscrollChk; - @FXML private Button clearBtn; - @FXML private TextField maxLinesField; - @FXML private TextArea textArea; - - private static final double EPS = 0.5; - private ScrollBar vBar; - - private static final int MIN_LINES = 10, MAX_LINES = 10_000, - BACKLOG = 1_000, CHUNK = 32_768; - - private int maxLines = 1_000; - private final ConsoleTextBuffer textBuf = new ConsoleTextBuffer(MAX_LINES); - - private final RunEngineService svc = new RunEngineService(); - private final ExecutorService io = - Executors.newSingleThreadExecutor(r -> new Thread(r,"console-reader")); - private static final ObjectMapper JSON = new ObjectMapper(); - - private volatile boolean stop = true; - private volatile String lastUid = "ALL"; - private Instant lastLine = Instant.EPOCH; - - private ScheduledFuture pollTask; - private volatile long lastProgrammaticScroll = 0; - private final StringBuilder wsTextBuffer = new StringBuilder(); - private final Object wsTextLock = new Object(); - - private QueueServerWebSocket consoleWs; - private volatile boolean isRunning = false; - private javafx.beans.value.ChangeListener statusListener; - - @Override public void initialize(URL url, ResourceBundle rb) { - // Register shutdown callback for app lifecycle management - AppLifecycle.registerShutdown(this::shutdown); - - textArea.setEditable(false); - textArea.setStyle("-fx-font-family: monospace"); - - // Add visual indicator when autoscroll is unchecked - autoscrollChk.selectedProperty().addListener((obs, wasSelected, isSelected) -> { - if (isSelected) { - // Checked - normal appearance - autoscrollChk.setStyle(""); - } else { - // Unchecked - red bold text to make it obvious - autoscrollChk.setStyle("-fx-text-fill: red; -fx-font-weight: bold;"); - } - }); - // Set initial style based on current state - if (!autoscrollChk.isSelected()) { - autoscrollChk.setStyle("-fx-text-fill: red; -fx-font-weight: bold;"); - } - - // Detect ANY user scroll interaction - mouse wheel, touchpad, scrollbar drag - textArea.setOnScroll(event -> { - // Any scroll event disables autoscroll - if (autoscrollChk.isSelected()) { - autoscrollChk.setSelected(false); - } - }); - - textArea.sceneProperty().addListener((o,ov,nv)->{ - if(nv==null)return; - Platform.runLater(()->{ - vBar =(ScrollBar)textArea.lookup(".scroll-bar:vertical"); - if(vBar!=null){ - vBar.valueProperty().addListener(this::scrollbarChanged); - - // Detect when user clicks anywhere on scrollbar - vBar.setOnMousePressed(event -> { - if (autoscrollChk.isSelected()) { - autoscrollChk.setSelected(false); - } - }); - - // Detect when user drags the scrollbar - vBar.setOnMouseDragged(event -> { - if (autoscrollChk.isSelected()) { - autoscrollChk.setSelected(false); - } - }); - } - }); - }); - - StringConverter cv = new IntegerStringConverter(); - TextFormatter tf = new TextFormatter<>(cv, maxLines, - c->c.getControlNewText().matches("\\d*")?c:null); - maxLinesField.setTextFormatter(tf); - - tf.valueProperty().addListener((o,ov,nv)->{ - maxLines = clamp(nv==null?MIN_LINES:nv); - maxLinesField.setText(String.valueOf(maxLines)); - render(); - }); - maxLinesField.focusedProperty().addListener((o,ov,nv)->{ - if(!nv) maxLinesField.setText(String.valueOf(maxLines)); - }); - - clearBtn.setOnAction(e->{ synchronized(textBuf){ textBuf.clear(); } render(); }); - - // Only start/stop on null <-> non-null transitions, not on every status change - statusListener = (o,oldS,newS)-> { - boolean wasConnected = oldS != null; - boolean isConnected = newS != null; - - // Only act on state transitions - if (wasConnected != isConnected) { - java.util.logging.Logger.getLogger(getClass().getPackageName()) - .log(java.util.logging.Level.FINE, - "Console monitor state transition: wasConnected=" + wasConnected + - ", isConnected=" + isConnected); - Platform.runLater(()-> { - if (isConnected) start(); - else stop(); - }); - } - }; - StatusBus.addListener(statusListener); - } - - public void shutdown(){ - // Remove status listener first to prevent any further callbacks - if (statusListener != null) { - StatusBus.removeListener(statusListener); - statusListener = null; - } - stop(); - if (consoleWs != null) { - consoleWs.close(); - } - io.shutdownNow(); - } - - private void start(){ - // Prevent starting if already running - if(isRunning) { - java.util.logging.Logger.getLogger(getClass().getPackageName()) - .log(java.util.logging.Level.FINE, "Console monitor start() called but already running, ignoring"); - return; - } - - if(pollTask!=null || (consoleWs != null && consoleWs.isConnected())) { - java.util.logging.Logger.getLogger(getClass().getPackageName()) - .log(java.util.logging.Level.FINE, "Console monitor start() called but already have active connections, ignoring"); - return; - } - - java.util.logging.Logger.getLogger(getClass().getPackageName()) - .log(java.util.logging.Level.FINE, "Console monitor starting"); - stop=false; synchronized(textBuf){ textBuf.clear(); } lastUid="ALL"; - synchronized (wsTextLock) { - wsTextBuffer.setLength(0); - } - - isRunning = true; - - // Load backlog in background thread to avoid blocking JavaFX thread - io.submit(this::loadBacklog); - - if (Preferences.use_websockets) { - startWebSocket(); - } else { - startStream(); - pollTask = PollCenter.everyMs(Preferences.update_interval_ms, this::poll); - } - } - - private void stop(){ - if(!isRunning) { - java.util.logging.Logger.getLogger(getClass().getPackageName()) - .log(java.util.logging.Level.FINE, "Console monitor stop() called but not running, ignoring"); - return; // Already stopped - } - - java.util.logging.Logger.getLogger(getClass().getPackageName()) - .log(java.util.logging.Level.FINE, "Console monitor stopping"); - - if(pollTask!=null){ pollTask.cancel(true); pollTask=null; } - if(consoleWs != null) { consoleWs.disconnect(); } - synchronized (wsTextLock) { - wsTextBuffer.setLength(0); - } - stop=true; - isRunning = false; - // Don't clear textBuf - keep last console output visible for users - } - - private void loadBacklog(){ - try{ - // These HTTP calls run in background thread (io executor) - String t = svc.consoleOutput(BACKLOG).text(); - String uid = svc.consoleOutputUid().uid(); - - // Process text in background thread (expensive string parsing) - if(!t.isEmpty()) { - synchronized (textBuf) { - textBuf.addMessage(t); - } - } - - // Update UI on JavaFX thread (just rendering) - Platform.runLater(() -> { - lastUid = uid; - render(); - - // Force scroll to bottom on initial load if autoscroll is enabled - // Multiple runLaters to ensure UI is fully laid out and text is rendered - if (autoscrollChk.isSelected()) { - Platform.runLater(() -> { - Platform.runLater(() -> { - Platform.runLater(() -> { - lastProgrammaticScroll = System.currentTimeMillis(); - textArea.positionCaret(textArea.getLength()); - textArea.setScrollTop(Double.MAX_VALUE); - if (vBar != null) { - vBar.setValue(vBar.getMax()); - } - }); - }); - }); - } - }); - }catch(Exception ignore){} - } - - private void startWebSocket(){ - consoleWs = svc.createConsoleOutputWebSocket(); - - // Buffer incoming WebSocket messages without immediately updating UI - consoleWs.addListener(msg -> { - String text = msg.msg(); - if (text != null && !text.isEmpty()) { - synchronized (wsTextLock) { - wsTextBuffer.append(text); - } - } - }); - - consoleWs.connect(); - - // Schedule throttled UI updates at the configured interval - pollTask = PollCenter.everyMs(Preferences.update_interval_ms, () -> { - String bufferedText; - synchronized (wsTextLock) { - if (wsTextBuffer.length() == 0) { - return; // Nothing to render - } - bufferedText = wsTextBuffer.toString(); - wsTextBuffer.setLength(0); - } - - Platform.runLater(() -> { - synchronized (textBuf) { - textBuf.addMessage(bufferedText); - } - render(); - lastLine = Instant.now(); - }); - }); - } - - private void startStream(){ - Task job = new Task<>(){ - @Override protected Void call(){ - try(var br = new BufferedReader( - new InputStreamReader(svc.streamConsoleOutput(), - StandardCharsets.UTF_8))){ - StringBuilder chunk = new StringBuilder(CHUNK); - for(String ln; !stop && (ln=br.readLine())!=null; ){ - if(!ln.isBlank()) - chunk.append(JSON.readTree(ln).path("msg").asText()); - if(chunk.length()>CHUNK){ - String out=chunk.toString(); chunk.setLength(0); - Platform.runLater(()->{ synchronized(textBuf){ textBuf.addMessage(out); } render(); }); - } - } - if(chunk.length()>0) - Platform.runLater(()->{ synchronized(textBuf){ textBuf.addMessage(chunk.toString()); } render(); }); - }catch(Exception ignore){} - return null; - } - }; - io.submit(job); - } - - private void poll(){ - if(stop||Instant.now().minusMillis(Preferences.update_interval_ms).isBefore(lastLine))return; - try{ - ConsoleOutputUpdate u = svc.consoleOutputUpdate(lastUid); - if(u.consoleOutputMsgs()!=null){ - StringBuilder sb=new StringBuilder(); - for(Map m:u.consoleOutputMsgs()) - sb.append((String)m.get("msg")); - if(sb.length()!=0){ synchronized(textBuf){ textBuf.addMessage(sb.toString()); } render(); } - } - lastUid=u.lastMsgUid(); - }catch(Exception ignore){} - } - - private void render() { - - final boolean wantBottom = autoscrollChk.isSelected(); - - int keepCaret = textArea.getCaretPosition(); - double keepScrollY = textArea.getScrollTop(); - - String text; - synchronized (textBuf) { - text = textBuf.tail(maxLines); - } - textArea.replaceText(0, textArea.getLength(), text); - - if (wantBottom) { - lastProgrammaticScroll = System.currentTimeMillis(); - Platform.runLater(() -> { - lastProgrammaticScroll = System.currentTimeMillis(); - textArea.positionCaret(textArea.getLength()); - textArea.setScrollTop(Double.MAX_VALUE); - if (vBar != null) vBar.setValue(vBar.getMax()); - }); - } else { - lastProgrammaticScroll = System.currentTimeMillis(); - textArea.positionCaret(Math.min(keepCaret, textArea.getLength())); - double y = keepScrollY; - Platform.runLater(() -> { - lastProgrammaticScroll = System.currentTimeMillis(); - textArea.setScrollTop(y); - }); - } - - lastLine = Instant.now(); - } - - private void scrollbarChanged(ObservableValue obs, - Number oldVal, Number newVal) { - - if (vBar == null) return; - - // If autoscroll is on, check if user scrolled away from bottom - if (autoscrollChk.isSelected()) { - // Ignore scroll events that happen within 100ms of programmatic scrolling - long timeSinceProgrammaticScroll = System.currentTimeMillis() - lastProgrammaticScroll; - if (timeSinceProgrammaticScroll < 100) { - return; // Too soon after programmatic scroll, ignore - } - - // Check if we're not at the bottom anymore (with small tolerance) - boolean atBottom = (vBar.getMax() - newVal.doubleValue()) < 2.0; - - // If not at bottom, user must have scrolled up - uncheck autoscroll - if (!atBottom) { - autoscrollChk.setSelected(false); - } - } - } - - private static int clamp(int v){ return Math.max(MIN_LINES,Math.min(MAX_LINES,v)); } - - private static final class ConsoleTextBuffer { - - private final int hardLimit; - private final List buf = new ArrayList<>(); - - private int line = 0; - private int pos = 0; - - private static final String NL = "\n"; - private static final String CR = "\r"; - private static final String UP_ONE_LINE = "\u001b[A"; // ESC[A - - ConsoleTextBuffer(int hardLimit) { - this.hardLimit = Math.max(hardLimit, 0); - } - - void clear() { - buf.clear(); - line = pos = 0; - } - - void addMessage(String msg) { - while (!msg.isEmpty()) { - // Find next control sequence - int nlIdx = msg.indexOf(NL); - int crIdx = msg.indexOf(CR); - int upIdx = msg.indexOf(UP_ONE_LINE); - - int next = minPos(nlIdx, crIdx, upIdx); - if (next < 0) next = msg.length(); - - // ---------------------------------------------------- - // FRAGMENT: plain text until control char - // ---------------------------------------------------- - if (next != 0) { - String frag = msg.substring(0, next); - msg = msg.substring(next); - - // Ensure we have at least one line - if (buf.isEmpty()) { - buf.add(""); - } - - ensureLineExists(line); - - // Extend current line with spaces if cursor is past EOL - int lineLen = buf.get(line).length(); - if (lineLen < pos) { - buf.set(line, buf.get(line) + " ".repeat(pos - lineLen)); - } - - // Insert/overwrite fragment at current position - String currentLine = buf.get(line); - String before = currentLine.substring(0, Math.min(pos, currentLine.length())); - String after = currentLine.substring(Math.min(pos + frag.length(), currentLine.length())); - buf.set(line, before + frag + after); - - pos += frag.length(); - continue; - } - - // ---------------------------------------------------- - // CONTROL SEQUENCES - // ---------------------------------------------------- - - if (nlIdx == 0) { - // Newline: move to next line - line++; - if (line >= buf.size()) { - buf.add(""); - } - pos = 0; - msg = msg.substring(NL.length()); - } - else if (crIdx == 0) { - // Carriage return: move to beginning of current line - pos = 0; - msg = msg.substring(CR.length()); - } - else if (upIdx == 0) { - // Move up one line - if (line > 0) { - line--; - } - pos = 0; - msg = msg.substring(UP_ONE_LINE.length()); - } - else { - // Shouldn't happen, but handle gracefully - if (!msg.isEmpty()) { - ensureLineExists(line); - buf.set(line, buf.get(line) + msg.charAt(0)); - pos++; - msg = msg.substring(1); - } - } - } - trim(); - } - - String tail(int n) { - if (n <= 0) return ""; - - // Remove trailing empty line if present - int visible = buf.size(); - if (visible > 0 && buf.get(visible - 1).isEmpty()) { - visible--; - } - - int start = Math.max(0, visible - n); - StringBuilder out = new StringBuilder(); - for (int i = start; i < visible; i++) { - out.append(buf.get(i)); - if (i + 1 < visible) out.append('\n'); - } - return out.toString(); - } - - private void ensureLineExists(int idx) { - while (buf.size() <= idx) { - buf.add(""); - } - } - - private void trim() { - // Keep some buffer beyond hardLimit to avoid constant trimming - int maxAllowed = hardLimit + 100; - while (buf.size() > maxAllowed) { - buf.remove(0); - line = Math.max(0, line - 1); - } - } - - private static int minPos(int... p) { - int m = Integer.MAX_VALUE; - for (int v : p) { - if (v >= 0 && v < m) m = v; - } - return m == Integer.MAX_VALUE ? -1 : m; - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java deleted file mode 100644 index a5d9df3bc2..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.application.Platform; -import javafx.beans.value.ObservableValue; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; - -import java.net.URL; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class ReEnvironmentControlsController implements Initializable { - - @FXML private Button openBtn; - @FXML private Button closeBtn; - @FXML private Button destroyBtn; - - private final RunEngineService svc = new RunEngineService(); - private static final Logger logger = Logger.getLogger(ReEnvironmentControlsController.class.getPackageName()); - - @Override public void initialize(URL url, ResourceBundle rb) { - StatusBus.addListener(this::onStatus); - // Check current value in case status was already set before listener added - refreshButtons(StatusBus.latest().get()); - } - - private void onStatus(ObservableValue src, Object oldV, Object newV) { - Platform.runLater(() -> refreshButtons(newV)); - } - - @SuppressWarnings("unchecked") - private void refreshButtons(Object statusObj) { - - boolean connected = statusObj != null; - boolean workerExists = false; - String managerState = null; - - if (statusObj instanceof StatusResponse s) { // preferred - workerExists = s.workerEnvironmentExists(); - managerState = s.managerState(); - } else if (statusObj instanceof Map m) { // fall-back - workerExists = Boolean.TRUE.equals(m.get("worker_environment_exists")); - managerState = String.valueOf(m.get("manager_state")); - } - - boolean idle = "idle".equals(managerState); - - openBtn .setDisable(!(connected && !workerExists && idle)); - closeBtn .setDisable(!(connected && workerExists && idle)); - destroyBtn.setDisable(!(connected && workerExists && idle)); - } - - @FXML private void open() { - try { svc.environmentOpen(); } - catch (Exception ex) { logger.log(Level.WARNING, "environmentOpen: " + ex); } - } - - @FXML private void close() { - try { svc.environmentClose(); } - catch (Exception ex) { logger.log(Level.WARNING, "environmentClose: " + ex); } - } - - @FXML private void destroy() { - Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle("Destroy Environment"); - alert.setHeaderText("Are you sure you want to destroy the environment?"); - alert.setContentText("This action cannot be undone and will permanently destroy the current environment."); - - alert.showAndWait().ifPresent(response -> { - if (response == ButtonType.OK) { - try { - svc.environmentDestroy(); - logger.log(Level.FINE, "Environment destroyed successfully"); - } catch (Exception ex) { - logger.log(Level.WARNING, "environmentDestroy: " + ex); - } - } - }); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java deleted file mode 100644 index 83e20c3f79..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.application.Platform; -import javafx.beans.value.ObservableValue; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Button; - -import java.net.URL; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class ReExecutionControlsController implements Initializable { - - @FXML private Button pauseDefBtn, pauseImmBtn, resumeBtn, ctrlcBtn, - stopBtn, abortBtn, haltBtn; - - private final RunEngineService svc = new RunEngineService(); - private static final Logger logger = Logger.getLogger(ReExecutionControlsController.class.getPackageName()); - - @Override public void initialize(URL url, ResourceBundle rb) { - - StatusBus.addListener(this::onStatus); - // Check current value in case status was already set before listener added - refreshButtons(StatusBus.latest().get()); - - pauseDefBtn.setOnAction(e -> call("rePause deferred", - () -> { - try { - svc.rePause("deferred"); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - })); // server default=deferred - pauseImmBtn.setOnAction(e -> call("rePause immediate", - () -> { - try { - svc.rePause("immediate"); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - })); // same endpoint (no body) - resumeBtn.setOnAction(e -> call("reResume", () -> { - try { svc.reResume(); } catch (Exception ex) { throw new RuntimeException(ex); } - })); - stopBtn.setOnAction(e -> call("reStop", () -> { - try { svc.reStop(); } catch (Exception ex) { throw new RuntimeException(ex); } - })); - abortBtn.setOnAction(e -> call("reAbort", () -> { - try { svc.reAbort(); } catch (Exception ex) { throw new RuntimeException(ex); } - })); - haltBtn.setOnAction(e -> call("reHalt", () -> { - try { svc.reHalt(); } catch (Exception ex) { throw new RuntimeException(ex); } - })); - ctrlcBtn.setOnAction(e -> call("kernelInterrupt", () -> { - try { svc.kernelInterrupt(); } catch (Exception ex) { throw new RuntimeException(ex); } - })); - } - - - private void onStatus(ObservableValue src, Object oldV, Object newV) { - Platform.runLater(() -> refreshButtons(newV)); - } - - @SuppressWarnings("unchecked") - private void refreshButtons(Object statusObj) { - boolean connected = statusObj != null; - - boolean workerExists = false; - String managerState = null; - String reState = null; - String ipKernelState = null; - - if (statusObj instanceof StatusResponse s) { - workerExists = s.workerEnvironmentExists(); - managerState = s.managerState(); - reState = s.reState(); - ipKernelState = s.ipKernelState(); - } - else if (statusObj instanceof Map m) { - workerExists = Boolean.TRUE.equals(m.get("worker_environment_exists")); - managerState = String.valueOf(m.get("manager_state")); - reState = String.valueOf(m.get("re_state")); - ipKernelState = String.valueOf(m.get("ip_kernel_state")); - } - - boolean pausePossible = - "executing_queue".equals(managerState) || "running".equals(reState); - - pauseDefBtn.setDisable(!(connected && workerExists && pausePossible)); - pauseImmBtn.setDisable(!(connected && workerExists && pausePossible)); - - boolean paused = "paused".equals(managerState); - resumeBtn.setDisable(!(connected && workerExists && paused)); - - stopBtn .setDisable(!(connected && workerExists && paused)); - abortBtn.setDisable(!(connected && workerExists && paused)); - haltBtn .setDisable(!(connected && workerExists && paused)); - - boolean ipIdleOrBusy = "idle".equals(ipKernelState) || "busy".equals(ipKernelState); - boolean ctrlCEnabled = connected && workerExists && - !"executing_queue".equals(managerState) && - !"running".equals(reState) && - ipIdleOrBusy; - ctrlcBtn.setDisable(!ctrlCEnabled); - } - - private void call(String label, Runnable r) { - try { r.run(); } - catch (Exception ex) { logger.log(Level.WARNING, label + ": " + ex); } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java deleted file mode 100644 index 1b6aeb014a..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java +++ /dev/null @@ -1,419 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.applications.queueserver.Preferences; -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.api.StatusWsMessage; -import org.phoebus.applications.queueserver.client.QueueServerWebSocket; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.AppLifecycle; -import org.phoebus.applications.queueserver.util.PlansCache; -import org.phoebus.applications.queueserver.util.PollCenter; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.application.Platform; -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.control.ToggleButton; - -import java.util.Map; -import java.util.concurrent.ScheduledFuture; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class ReManagerConnectionController { - - @FXML private ToggleButton autoConnectToggle; - @FXML private Label connectionStatusLabel; - - private final RunEngineService svc = new RunEngineService(); - private final ObjectMapper mapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - private ScheduledFuture pollTask; - private ScheduledFuture reconnectTask; - private ScheduledFuture healthCheckTask; - private QueueServerWebSocket statusWs; - private volatile StatusResponse latestStatus = null; - private volatile boolean websocketConnected = false; - private volatile boolean attemptingConnection = false; - private volatile long lastStatusUpdateTime = 0; - private volatile long connectionAttemptStartTime = 0; - private volatile int reconnectAttempts = 0; - private volatile ConnectionState currentState = ConnectionState.DISCONNECTED; - private static final Logger logger = Logger.getLogger(ReManagerConnectionController.class.getPackageName()); - - // Status message timeout - if no status received for this long, show RED error - private static final long STATUS_TIMEOUT_MS = 3000; - - // Connection attempt timeout - if no connection after this long, show NETWORK ERROR - private static final long CONNECTION_ATTEMPT_TIMEOUT_MS = 5000; - - // Reconnect interval - how often to retry connection when it fails - private static final long RECONNECT_INTERVAL_MS = 5000; - - // Connection states - private enum ConnectionState { - DISCONNECTED, // Grey - User manually disabled auto-connect - CONNECTING, // Grey - Attempting to establish connection - NETWORK_ERROR, // Dark blue - WebSocket can't connect (auth/network issue) - NO_STATUS, // Red - WebSocket connected but no status messages from RE Manager - CONNECTED // Green - WebSocket connected AND receiving status messages - } - - @FXML - public void initialize() { - // Register shutdown callback for app lifecycle management - AppLifecycle.registerShutdown(this::stop); - - // Start in auto-connect mode (toggle is selected by default in FXML) - if (autoConnectToggle.isSelected()) { - start(); - } - } - - @FXML - private void toggleConnection() { - if (autoConnectToggle.isSelected()) { - // User enabled auto-connect - start(); - } else { - // User disabled auto-connect - stop(); - } - } - - private void start() { - if (pollTask != null && !pollTask.isDone()) return; // already running - if (statusWs != null && statusWs.isConnected()) return; // already connected - - websocketConnected = false; - attemptingConnection = false; - latestStatus = null; - lastStatusUpdateTime = 0; - connectionAttemptStartTime = 0; - reconnectAttempts = 0; - - if (Preferences.use_websockets) { - logger.log(Level.INFO, "Starting status WebSocket connection"); - attemptWebSocketConnection(); - } else { - logger.log(Level.INFO, "Starting status polling every " + Preferences.update_interval_ms + " milliseconds"); - attemptingConnection = true; - connectionAttemptStartTime = System.currentTimeMillis(); - pollTask = PollCenter.everyMs(Preferences.update_interval_ms, - this::queryStatusOnce, // background - this::updateWidgets); // FX thread - } - - // Start health check to monitor connection state - if (healthCheckTask == null) { - healthCheckTask = PollCenter.everyMs(500, this::checkConnectionHealth); - } - } - - private void attemptWebSocketConnection() { - try { - attemptingConnection = true; - connectionAttemptStartTime = System.currentTimeMillis(); - - statusWs = svc.createStatusWebSocket(); - - // Buffer incoming WebSocket messages without immediately updating UI - statusWs.addListener(msg -> { - Map statusMap = msg.status(); - if (statusMap != null) { - try { - // Convert Map to StatusResponse and buffer it - latestStatus = mapper.convertValue(statusMap, StatusResponse.class); - // Record timestamp when we receive fresh data from WebSocket - lastStatusUpdateTime = System.currentTimeMillis(); - - // Mark WebSocket as connected on first successful message - if (!websocketConnected) { - websocketConnected = true; - attemptingConnection = false; - reconnectAttempts = 0; - logger.log(Level.INFO, "WebSocket connection established"); - } - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to parse status from WebSocket", e); - } - } - }); - - statusWs.connect(); - - // Schedule throttled UI updates at the configured interval - pollTask = PollCenter.everyMs(Preferences.update_interval_ms, () -> { - // Check if WebSocket is still connected - if (statusWs != null && !statusWs.isConnected()) { - // WebSocket closed - if (websocketConnected) { - logger.log(Level.WARNING, "WebSocket connection lost"); - websocketConnected = false; - attemptingConnection = false; - - // Auto-reconnect if toggle is still enabled - if (autoConnectToggle.isSelected()) { - scheduleReconnect(); - } - } - } - - StatusResponse status = latestStatus; - if (status != null) { - Platform.runLater(() -> updateWidgets(status)); - } - }); - } catch (Exception e) { - logger.log(Level.SEVERE, "Failed to create WebSocket connection", e); - websocketConnected = false; - attemptingConnection = false; - - // Auto-reconnect if toggle is still enabled - if (autoConnectToggle.isSelected()) { - scheduleReconnect(); - } - } - } - - private void scheduleReconnect() { - // Don't reconnect if user disabled auto-connect - if (!autoConnectToggle.isSelected()) { - return; - } - - // Don't schedule if already scheduled and not done - if (reconnectTask != null && !reconnectTask.isDone()) { - logger.log(Level.FINE, "Reconnect already scheduled, skipping"); - return; - } - - // Don't schedule if already attempting connection - if (attemptingConnection) { - logger.log(Level.FINE, "Connection attempt in progress, skipping reconnect schedule"); - return; - } - - // Cancel any existing reconnect task - if (reconnectTask != null) { - reconnectTask.cancel(false); - } - - reconnectAttempts++; - - logger.log(Level.INFO, "Scheduling reconnect attempt #" + reconnectAttempts + " in " + RECONNECT_INTERVAL_MS + "ms"); - - reconnectTask = PollCenter.afterMs((int) RECONNECT_INTERVAL_MS, () -> { - if (autoConnectToggle.isSelected() && !attemptingConnection) { - logger.log(Level.INFO, "Attempting to reconnect (attempt #" + reconnectAttempts + ")"); - - // Clean up existing connection - if (statusWs != null) { - statusWs.disconnect(); - statusWs = null; - } - if (pollTask != null) { - pollTask.cancel(false); - pollTask = null; - } - - // Reset state before attempting reconnection - websocketConnected = false; - - // Try to reconnect - attemptWebSocketConnection(); - } - }); - } - - private StatusResponse queryStatusOnce() { - try { - StatusResponse status = svc.status(); - if (status != null) { - lastStatusUpdateTime = System.currentTimeMillis(); - if (attemptingConnection) { - attemptingConnection = false; - reconnectAttempts = 0; - logger.log(Level.INFO, "HTTP connection established"); - } - } - return status; - } catch (Exception ex) { - logger.log(Level.FINE, "Status query failed: " + ex.getMessage()); - return null; - } - } - - private void checkConnectionHealth() { - ConnectionState newState; - - if (!autoConnectToggle.isSelected()) { - // User disabled auto-connect - newState = ConnectionState.DISCONNECTED; - } else if (Preferences.use_websockets) { - // WebSocket mode - if (attemptingConnection) { - // Currently attempting to connect - long timeSinceAttemptStart = System.currentTimeMillis() - connectionAttemptStartTime; - if (timeSinceAttemptStart > CONNECTION_ATTEMPT_TIMEOUT_MS) { - // Connection attempt timed out - trigger reconnect - attemptingConnection = false; - newState = ConnectionState.NETWORK_ERROR; - - // Clean up failed connection attempt - if (pollTask != null) { - pollTask.cancel(false); - pollTask = null; - } - if (statusWs != null) { - statusWs.disconnect(); - statusWs = null; - } - - scheduleReconnect(); - } else { - // Still trying to connect - newState = ConnectionState.CONNECTING; - } - } else if (!websocketConnected) { - // Not attempting and not connected - newState = ConnectionState.NETWORK_ERROR; - - // Only schedule reconnect if no reconnect task is pending - if ((reconnectTask == null || reconnectTask.isDone()) && - (statusWs == null || !statusWs.isConnected())) { - scheduleReconnect(); - } - } else if (lastStatusUpdateTime == 0) { - // WebSocket connected but no status received yet - still establishing - newState = ConnectionState.CONNECTING; - } else { - long timeSinceLastUpdate = System.currentTimeMillis() - lastStatusUpdateTime; - if (timeSinceLastUpdate > STATUS_TIMEOUT_MS) { - // WebSocket connected but no recent status - RE Manager issue - newState = ConnectionState.NO_STATUS; - } else { - // All good - connected and receiving status - newState = ConnectionState.CONNECTED; - } - } - } else { - // HTTP polling mode - if (attemptingConnection) { - // Currently attempting to connect - long timeSinceAttemptStart = System.currentTimeMillis() - connectionAttemptStartTime; - if (timeSinceAttemptStart > CONNECTION_ATTEMPT_TIMEOUT_MS) { - // Connection attempt timed out - attemptingConnection = false; - newState = ConnectionState.NETWORK_ERROR; - } else { - // Still trying to connect - newState = ConnectionState.CONNECTING; - } - } else if (lastStatusUpdateTime == 0) { - // No successful status query yet - newState = ConnectionState.NETWORK_ERROR; - } else { - long timeSinceLastUpdate = System.currentTimeMillis() - lastStatusUpdateTime; - if (timeSinceLastUpdate > STATUS_TIMEOUT_MS) { - // No recent status - newState = ConnectionState.NETWORK_ERROR; - } else { - // Receiving status - newState = ConnectionState.CONNECTED; - } - } - } - - // Only update UI if state actually changed - if (newState != currentState) { - ConnectionState oldState = currentState; - currentState = newState; - logger.log(Level.FINE, "Connection state change: " + oldState + " → " + newState); - Platform.runLater(() -> updateConnectionState(newState)); - } - } - - private void stop() { - logger.log(Level.INFO, "Stopping status monitoring"); - - if (reconnectTask != null) { - reconnectTask.cancel(true); - reconnectTask = null; - } - if (healthCheckTask != null) { - healthCheckTask.cancel(true); - healthCheckTask = null; - } - if (pollTask != null) { - pollTask.cancel(true); - pollTask = null; - } - if (statusWs != null) { - statusWs.disconnect(); - statusWs = null; - } - - websocketConnected = false; - reconnectAttempts = 0; - - // Don't clear latestStatus or push null to StatusBus - // This preserves the last known state in all UI widgets - - currentState = ConnectionState.DISCONNECTED; - updateConnectionState(ConnectionState.DISCONNECTED); - } - - private void updateConnectionState(ConnectionState state) { - switch (state) { - case DISCONNECTED: - connectionStatusLabel.setText("OFFLINE"); - connectionStatusLabel.setStyle("-fx-text-fill: grey;"); - autoConnectToggle.setText("Connect"); - latestStatus = null; // Clear cached status - StatusBus.push(null); // Disable all controls - break; - - case CONNECTING: - connectionStatusLabel.setText("CONNECTING"); - connectionStatusLabel.setStyle("-fx-text-fill: grey;"); - autoConnectToggle.setText("Disconnect"); - // Don't push null here - preserve last known status during reconnection - // This prevents unnecessary UI flickering and console monitor restarts - break; - - case NETWORK_ERROR: - connectionStatusLabel.setText("NETWORK"); - connectionStatusLabel.setStyle("-fx-text-fill: #00008B;"); // Dark blue - autoConnectToggle.setText("Disconnect"); - latestStatus = null; // Clear cached status to prevent stale data from being pushed - StatusBus.push(null); // Disable all controls - break; - - case NO_STATUS: - connectionStatusLabel.setText("STATUS"); - connectionStatusLabel.setStyle("-fx-text-fill: red;"); - autoConnectToggle.setText("Disconnect"); - latestStatus = null; // Clear cached status - StatusBus.push(null); // Disable all controls - break; - - case CONNECTED: - connectionStatusLabel.setText("CONNECTED"); - connectionStatusLabel.setStyle("-fx-text-fill: green;"); - autoConnectToggle.setText("Disconnect"); - // Load allowed plans into shared cache (only fetches once) - PlansCache.loadIfNeeded(); - // Don't push to StatusBus here - let updateWidgets do it - break; - } - } - - private void updateWidgets(StatusResponse s) { - if (s != null) { - logger.log(Level.FINEST, "Status update: manager_state=" + s.managerState()); - StatusBus.push(s); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java deleted file mode 100644 index d1fc80df11..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java +++ /dev/null @@ -1,1398 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.DateUtil; -import org.phoebus.applications.queueserver.api.*; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.PlansCache; -import org.phoebus.applications.queueserver.util.QueueItemSelectionEvent; -import org.phoebus.applications.queueserver.util.StatusBus; -import org.phoebus.applications.queueserver.view.PlanEditEvent; -import org.phoebus.applications.queueserver.view.TabSwitchEvent; -import org.phoebus.applications.queueserver.view.ItemUpdateEvent; -import org.phoebus.applications.queueserver.util.PythonParameterConverter; -import com.fasterxml.jackson.databind.ObjectMapper; -import javafx.application.Platform; -import javafx.geometry.Insets; -import javafx.scene.Scene; -import javafx.stage.Stage; -import javafx.scene.layout.VBox; -import javafx.scene.layout.HBox; -import javafx.scene.layout.GridPane; -import org.apache.poi.ss.usermodel.*; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.*; -import javafx.scene.layout.Region; -import javafx.scene.text.Text; -import javafx.scene.control.cell.CheckBoxTableCell; -import javafx.scene.control.cell.PropertyValueFactory; - -import java.net.URL; -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class RePlanEditorController implements Initializable { - - @FXML private Button addBtn; - @FXML private Button batchBtn; - @FXML private Button cancelBtn; - @FXML private TableColumn chkCol; - @FXML private ChoiceBox choiceBox; - @FXML private RadioButton instrRadBtn; - @FXML private TableColumn paramCol; - @FXML private RadioButton planRadBtn; - @FXML private Button resetBtn; - @FXML private Button saveBtn; - @FXML private TableView table; - @FXML private TableColumn valueCol; - - private final RunEngineService svc = new RunEngineService(); - private static final Logger logger = Logger.getLogger(RePlanEditorController.class.getPackageName()); - private final ObservableList parameterRows = FXCollections.observableArrayList(); - private final Map> allowedPlans = new HashMap<>(); - private final Map> allowedInstructions = new HashMap<>(); - private QueueItem currentItem; - private boolean isEditMode = false; - private String currentUser = "GUI Client"; - private String currentUserGroup = "root"; - private String currentItemSource = ""; - private boolean editorStateValid = false; - private final ObjectMapper objectMapper = new ObjectMapper(); - // Store original parameter values for reset functionality - private final Map originalParameterValues = new HashMap<>(); - - /** - * Get the Python converter for use in background threads ONLY. - * Uses the shared singleton instance. - * NEVER call this from JavaFX thread - it will cause UI freezes. - */ - private PythonParameterConverter getPythonConverter() { - PythonParameterConverter converter = PythonParameterConverter.getShared(); - if (converter == null) { - // Shared init not done yet - create inline (rare fallback) - converter = new PythonParameterConverter(); - } - return converter; - } - - private class EditableTableCell extends TableCell { - private TextField textField; - - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - - if (empty || getTableRow() == null || getTableRow().getItem() == null) { - setText(null); - setGraphic(null); - setStyle(""); - } else { - ParameterRow row = getTableRow().getItem(); - - if (isEditing()) { - if (textField != null) { - textField.setText(getString()); - } - setText(null); - setGraphic(textField); - } else { - setText(getString()); - setGraphic(null); - - // Style based on enabled state and validation - // NOTE: Use simple UI validation here - never call Python converter from JavaFX thread - if (!row.isEnabled()) { - setStyle("-fx-text-fill: grey;"); - } else if (!row.isValidForUI()) { - setStyle("-fx-text-fill: red;"); - } else { - setStyle(""); - } - - // Add tooltip with parameter description - if (row.getDescription() != null && !row.getDescription().isEmpty()) { - Tooltip tooltip = new Tooltip(row.getDescription()); - tooltip.setWrapText(true); - tooltip.setMaxWidth(300); - setTooltip(tooltip); - } - } - } - } - - @Override - public void startEdit() { - ParameterRow row = getTableRow().getItem(); - if (row != null && row.isEnabled()) { - super.startEdit(); - createTextField(); - setText(null); - setGraphic(textField); - textField.selectAll(); - textField.requestFocus(); - } - } - - @Override - public void cancelEdit() { - super.cancelEdit(); - setText(getString()); - setGraphic(null); - } - - @Override - public void commitEdit(String newValue) { - super.commitEdit(newValue); - ParameterRow row = getTableRow().getItem(); - if (row != null) { - row.setValue(newValue); - - // Update cell color based on Python validation - updateValidationColor(row); - - switchToEditingMode(); - updateButtonStates(); - } - } - - private void updateValidationColor(ParameterRow row) { - // NOTE: Use simple UI validation here - never call Python converter from JavaFX thread - if (!row.isEnabled()) { - setStyle("-fx-text-fill: grey;"); - } else if (row.isValidForUI()) { - setStyle(""); - } else { - setStyle("-fx-text-fill: red;"); - } - } - - private void createTextField() { - textField = new TextField(getString()); - textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2); - textField.setOnAction(e -> commitEdit(textField.getText())); - textField.focusedProperty().addListener((obs, oldVal, newVal) -> { - if (!newVal) { - commitEdit(textField.getText()); - } - }); - } - - private String getString() { - return getItem() == null ? "" : getItem(); - } - - // Make cell clickable to start editing - { - setOnMouseClicked(e -> { - if (getTableRow() != null && getTableRow().getItem() != null) { - ParameterRow row = getTableRow().getItem(); - if (row.isEnabled() && !isEditing()) { - startEdit(); - } - } - }); - } - } - - public static class ParameterRow { - private final SimpleStringProperty name; - private final SimpleBooleanProperty enabled; - private final SimpleStringProperty value; - private final SimpleStringProperty description; - private final SimpleBooleanProperty isOptional; - private final Object defaultValue; - - public ParameterRow(String name, boolean enabled, String value, String description, - boolean isOptional, Object defaultValue) { - this.name = new SimpleStringProperty(name); - this.enabled = new SimpleBooleanProperty(enabled); - this.value = new SimpleStringProperty(value != null ? value : ""); - this.description = new SimpleStringProperty(description != null ? description : ""); - this.isOptional = new SimpleBooleanProperty(isOptional); - this.defaultValue = defaultValue; - } - - public SimpleStringProperty nameProperty() { return name; } - public SimpleBooleanProperty enabledProperty() { return enabled; } - public SimpleStringProperty valueProperty() { return value; } - public SimpleStringProperty descriptionProperty() { return description; } - public SimpleBooleanProperty isOptionalProperty() { return isOptional; } - - public String getName() { return name.get(); } - public boolean isEnabled() { return enabled.get(); } - public void setEnabled(boolean enabled) { this.enabled.set(enabled); } - public String getValue() { return value.get(); } - public void setValue(String value) { this.value.set(value); } - public String getDescription() { return description.get(); } - public boolean isOptional() { return isOptional.get(); } - public Object getDefaultValue() { return defaultValue; } - - /** - * Simple UI validation that doesn't use Python converter. - * Only checks if required parameters have values. - * Use this for UI feedback (cell colors, button states). - */ - public boolean isValidForUI() { - if (!enabled.get()) { - return true; // Disabled parameters are always valid - } - - String valueStr = value.get(); - if (valueStr == null || valueStr.trim().isEmpty()) { - return isOptional.get(); // Empty is valid only if optional - } - - return true; // Has a value, assume valid for UI purposes - } - - /** - * Full validation using Python converter. - * NEVER call this from JavaFX thread - it will cause UI freezes! - * Only use in background threads before submitting to API. - */ - public boolean validate(PythonParameterConverter converter) { - if (!enabled.get()) { - return true; // Disabled parameters are always valid - } - - String valueStr = value.get(); - if (valueStr == null || valueStr.trim().isEmpty()) { - return isOptional.get(); - } - - // Validate using Python converter - try { - List testParams = List.of( - new PythonParameterConverter.ParameterInfo( - getName(), - valueStr, - true, - isOptional.get(), - getDefaultValue() - ) - ); - converter.convertParameters(testParams); - return true; - } catch (Exception e) { - return false; - } - } - - public boolean isEditable() { - // Parameter is editable if it's enabled or if it's required (not optional) - return enabled.get() || !isOptional.get(); - } - } - - @Override - public void initialize(URL location, ResourceBundle resources) { - initializeTable(); - initializeControls(); - initializeAllowedInstructions(); - - // Use shared plans cache - copy to local map when loaded - if (PlansCache.isLoaded()) { - allowedPlans.clear(); - allowedPlans.putAll(PlansCache.get()); - } - - // Listen for plans cache updates - PlansCache.addListener((o, oldV, newV) -> { - Platform.runLater(() -> { - allowedPlans.clear(); - if (newV != null) { - allowedPlans.putAll(newV); - } - }); - }); - - // Listen for status changes to update button states and clear on disconnect - StatusBus.addListener((o, oldV, newV) -> { - // Clear the editor state when disconnected - if (newV == null) { - Platform.runLater(this::cancelEdit); - } - Platform.runLater(this::updateButtonStates); - }); - - // Listen for edit requests from plan viewer - PlanEditEvent.getInstance().addListener(this::editItem); - } - - private void initializeAllowedInstructions() { - Map queueStopInstr = new HashMap<>(); - queueStopInstr.put("name", "queue_stop"); - queueStopInstr.put("description", "Stop execution of the queue."); - queueStopInstr.put("parameters", List.of()); - allowedInstructions.put("queue_stop", queueStopInstr); - } - - private void initializeTable() { - paramCol.setCellValueFactory(new PropertyValueFactory<>("name")); - chkCol.setCellValueFactory(new PropertyValueFactory<>("enabled")); - valueCol.setCellValueFactory(new PropertyValueFactory<>("value")); - - // Fix paramCol and chkCol widths - not resizable - paramCol.setMinWidth(120); - paramCol.setPrefWidth(120); - paramCol.setMaxWidth(120); - paramCol.setResizable(false); - - chkCol.setMinWidth(30); - chkCol.setPrefWidth(30); - chkCol.setMaxWidth(30); - chkCol.setResizable(false); - - // valueCol fills remaining space - valueCol.setMinWidth(50); - - // Add tooltips to parameter names - paramCol.setCellFactory(column -> { - TableCell cell = new TableCell() { - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || getTableRow() == null || getTableRow().getItem() == null) { - setText(null); - setTooltip(null); - } else { - setText(item); - ParameterRow row = getTableRow().getItem(); - if (row != null && row.getDescription() != null && !row.getDescription().isEmpty()) { - Tooltip tooltip = new Tooltip(row.getDescription()); - tooltip.setWrapText(true); - tooltip.setMaxWidth(300); - setTooltip(tooltip); - } - } - } - }; - return cell; - }); - - chkCol.setCellFactory(CheckBoxTableCell.forTableColumn(chkCol)); - chkCol.setEditable(true); - - valueCol.setCellFactory(column -> new EditableTableCell()); - - valueCol.setOnEditCommit(event -> { - ParameterRow row = event.getRowValue(); - row.setValue(event.getNewValue()); - switchToEditingMode(); - updateButtonStates(); - }); - - // Add listener for checkbox changes to enable/disable editing and trigger edit mode - chkCol.setOnEditCommit(event -> { - ParameterRow row = event.getRowValue(); - boolean isChecked = event.getNewValue(); - row.setEnabled(isChecked); - - // If unchecked, reset to default value; if checked and no value, set default - // NOTE: Use simple string conversion here - never call Python converter from JavaFX thread - if (!isChecked) { - Object defaultValue = row.getDefaultValue(); - row.setValue(defaultValue != null ? String.valueOf(defaultValue) : ""); - } else if (row.getValue().isEmpty() && row.getDefaultValue() != null) { - row.setValue(String.valueOf(row.getDefaultValue())); - } - - // Trigger edit mode when checkbox is changed - switchToEditingMode(); - - updateButtonStates(); - }); - - table.setItems(parameterRows); - table.setEditable(true); - - // Make Value column fill remaining space - table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - } - - private void initializeControls() { - ToggleGroup typeGroup = new ToggleGroup(); - planRadBtn.setToggleGroup(typeGroup); - instrRadBtn.setToggleGroup(typeGroup); - planRadBtn.setSelected(true); - - // Setup ChoiceBox to auto-size to fit content exactly - setupChoiceBoxAutoSizing(); - - // Lazy populate when user opens the dropdown (avoids UI freeze during connection) - choiceBox.setOnShowing(event -> { - if (choiceBox.getItems().isEmpty() && !allowedPlans.isEmpty()) { - populateChoiceBox(planRadBtn.isSelected()); - } - }); - - choiceBox.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { - if (newVal != null) { - loadParametersForSelection(newVal); - // Set tooltip for choice box with item description - setChoiceBoxTooltip(newVal); - } - updateButtonStates(); - // Resize ChoiceBox to fit new selection - resizeChoiceBoxToFitContent(); - }); - - planRadBtn.selectedProperty().addListener((obs, oldVal, newVal) -> { - if (newVal && !isEditMode) { - populateChoiceBox(true); - updateButtonStates(); - } - }); - - instrRadBtn.selectedProperty().addListener((obs, oldVal, newVal) -> { - if (newVal && !isEditMode) { - populateChoiceBox(false); - updateButtonStates(); - } - }); - - addBtn.setOnAction(e -> addItemToQueue()); - batchBtn.setOnAction(e -> openBatchUpload()); - saveBtn.setOnAction(e -> saveItem()); - resetBtn.setOnAction(e -> resetParametersToDefaults()); - cancelBtn.setOnAction(e -> cancelEdit()); - - updateButtonStates(); - } - - private void populateChoiceBox(boolean isPlans) { - choiceBox.getItems().clear(); - parameterRows.clear(); - - if (isPlans) { - choiceBox.getItems().addAll(allowedPlans.keySet()); - } else { - choiceBox.getItems().addAll(allowedInstructions.keySet()); - } - - Collections.sort(choiceBox.getItems()); - updateButtonStates(); - // Resize ChoiceBox after populating - resizeChoiceBoxToFitContent(); - } - - private void setupChoiceBoxAutoSizing() { - // Set minimum width - choiceBox.setMinWidth(Region.USE_PREF_SIZE); - choiceBox.setMaxWidth(Region.USE_PREF_SIZE); - } - - private void resizeChoiceBoxToFitContent() { - Platform.runLater(() -> { - String selectedText = choiceBox.getSelectionModel().getSelectedItem(); - if (selectedText != null && !selectedText.isEmpty()) { - // Calculate the width needed for the selected text - Text text = new Text(selectedText); - // Use default font since ChoiceBox doesn't have getFont() - double textWidth = text.getLayoutBounds().getWidth(); - - // Add padding for dropdown arrow and margins (about 30px) - double newWidth = textWidth + 30; - - // Set the exact width needed - choiceBox.setPrefWidth(newWidth); - choiceBox.setMinWidth(newWidth); - choiceBox.setMaxWidth(newWidth); - } else { - // If nothing selected or empty, make it small (minimum size) - double minWidth = 80; // Small default size - choiceBox.setPrefWidth(minWidth); - choiceBox.setMinWidth(minWidth); - choiceBox.setMaxWidth(minWidth); - } - }); - } - - private void loadParametersForSelection(String selectedName) { - parameterRows.clear(); - - Map itemInfo; - if (planRadBtn.isSelected()) { - itemInfo = allowedPlans.get(selectedName); - } else { - itemInfo = allowedInstructions.get(selectedName); - } - - if (itemInfo == null) return; - - List> parameters = (List>) itemInfo.get("parameters"); - if (parameters == null) parameters = new ArrayList<>(); - - Map currentKwargs = new HashMap<>(); - if (isEditMode && currentItem != null) { - currentKwargs = currentItem.kwargs() != null ? currentItem.kwargs() : new HashMap<>(); - // Store original values for reset functionality - originalParameterValues.clear(); - originalParameterValues.putAll(currentKwargs); - } - - for (Map paramInfo : parameters) { - String paramName = (String) paramInfo.get("name"); - String description = (String) paramInfo.get("description"); - if (description == null || description.isEmpty()) { - description = "Description for parameter '" + paramName + "' was not found..."; - } - Object defaultValue = paramInfo.get("default"); - boolean isOptional = defaultValue != null || "VAR_POSITIONAL".equals(paramInfo.get("kind")) || - "VAR_KEYWORD".equals(paramInfo.get("kind")); - - String currentValue = ""; - boolean isEnabled = !isOptional; - - if (currentKwargs.containsKey(paramName)) { - Object value = currentKwargs.get(paramName); - currentValue = value != null ? PythonParameterConverter.toPythonRepr(value) : ""; - isEnabled = true; - } else if (defaultValue != null) { - // Use simple string conversion - Python parsing will happen in background thread later - // NOTE: Never call Python converter from JavaFX thread to avoid UI freezes - currentValue = String.valueOf(defaultValue); - } - - ParameterRow row = new ParameterRow(paramName, isEnabled, currentValue, description, isOptional, defaultValue); - parameterRows.add(row); - } - - if (isEditMode && currentItem != null && currentItem.result() != null) { - addMetadataAndResults(currentItem); - } - - updateButtonStates(); - } - - private void addMetadataAndResults(QueueItem item) { - if (item.result() == null) { - return; - } - - Map result = item.result(); - - // Add a separator row for metadata section - if (!result.isEmpty()) { - ParameterRow separator = new ParameterRow("--- Metadata & Results ---", false, "", - "Execution metadata and results (read-only)", false, null); - parameterRows.add(separator); - } - - // Add metadata fields as read-only rows - for (Map.Entry entry : result.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - String displayValue = formatResultValue(value); - String description = "Result field: " + key + " (read-only)"; - - ParameterRow row = new ParameterRow(key, false, displayValue, description, false, null); - parameterRows.add(row); - } - } - - private String formatResultValue(Object value) { - if (value == null) { - return "null"; - } - - if (value instanceof Map) { - Map map = (Map) value; - if (map.isEmpty()) { - return "{}"; - } - return "Map (" + map.size() + " entries)"; - } - - if (value instanceof List) { - List list = (List) value; - if (list.isEmpty()) { - return "[]"; - } - return "List (" + list.size() + " items)"; - } - - if (value instanceof String) { - String str = (String) value; - if (str.length() > 100) { - return str.substring(0, 97) + "..."; - } - return str; - } - - return String.valueOf(value); - } - - private boolean areParametersValid() { - // NOTE: Use simple UI validation here - never call Python converter from JavaFX thread - boolean allValid = true; - for (ParameterRow row : parameterRows) { - boolean rowValid = row.isValidForUI(); - if (!rowValid) { - allValid = false; - } - // Update visual validation state - updateRowValidationStyle(row, rowValid); - } - return allValid; - } - - private void updateRowValidationStyle(ParameterRow row, boolean isValid) { - // This would need to be called to update the cell styling - // For now, we'll handle this in the cell factory - Platform.runLater(() -> { - table.refresh(); - }); - } - - private void updateButtonStates() { - StatusResponse status = StatusBus.latest().get(); - boolean isConnected = status != null; - - boolean hasSelectedPlan = choiceBox.getSelectionModel().getSelectedItem() != null; - boolean isValid = areParametersValid(); - this.editorStateValid = isValid; - - // Disable controls when not connected (environment open not required) - table.setDisable(!isConnected); - planRadBtn.setDisable(!isConnected || isEditMode); - instrRadBtn.setDisable(!isConnected || isEditMode); - choiceBox.setDisable(!isConnected || isEditMode); - - addBtn.setDisable(!isConnected || !hasSelectedPlan || !isValid); - - saveBtn.setDisable(!isConnected || !isEditMode || !hasSelectedPlan || !isValid || - !"QUEUE ITEM".equals(currentItemSource)); - - batchBtn.setDisable(!isConnected); - - resetBtn.setDisable(!isConnected || !isEditMode); - cancelBtn.setDisable(!isConnected || !isEditMode); - } - - /** - * Build kwargs map using Python-based type conversion. - * All type conversion is handled by Python script using ast.literal_eval. - */ - private Map buildKwargsWithPython() { - List paramInfos = new ArrayList<>(); - - for (ParameterRow row : parameterRows) { - PythonParameterConverter.ParameterInfo paramInfo = - new PythonParameterConverter.ParameterInfo( - row.getName(), - row.getValue(), - row.isEnabled(), - row.isOptional(), - row.getDefaultValue() - ); - paramInfos.add(paramInfo); - } - - // Use Python to convert parameters - no Java fallback - return getPythonConverter().convertParameters(paramInfos); - } - - private void addItemToQueue() { - if (!areParametersValid()) { - showValidationError("Some parameters have invalid values. Please check the red fields."); - return; - } - - try { - String selectedName = choiceBox.getSelectionModel().getSelectedItem(); - if (selectedName == null) { - return; - } - - String itemType = planRadBtn.isSelected() ? "plan" : "instruction"; - - // Capture insert position before starting background thread - String afterUid = QueueItemSelectionEvent.getInstance().getLastSelectedUid(); - - new Thread(() -> { - try { - // Use Python-based parameter conversion (in background thread to avoid blocking UI) - Map kwargs = buildKwargsWithPython(); - - QueueItem item = new QueueItem( - itemType, - selectedName, - List.of(), - kwargs, - null, - currentUser, - currentUserGroup, - null - ); - - QueueItemAdd request = new QueueItemAdd( - new QueueItemAdd.Item(item.itemType(), item.name(), item.args(), item.kwargs()), - currentUser, - currentUserGroup, - afterUid - ); - - String newUid = svc.addItemGetUid(request); - QueueItemSelectionEvent.getInstance().requestSelectByUids(List.of(newUid)); - Platform.runLater(() -> { - // Clear parameters but preserve radio button selection - parameterRows.clear(); - choiceBox.getSelectionModel().clearSelection(); - // Don't reset radio button - keep current selection - populateChoiceBox(planRadBtn.isSelected()); - // Switch to view tab - TabSwitchEvent.getInstance().switchToTab("Plan Viewer"); - exitEditMode(); - showItemPreview(); - }); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to add item to queue", e); - Platform.runLater(() -> { - String errorMsg = e.getMessage(); - if (errorMsg == null || errorMsg.isEmpty()) { - errorMsg = e.getClass().getSimpleName(); - } - showValidationError("Failed to add item to queue: " + errorMsg); - }); - } - }).start(); - - } catch (Exception e) { - logger.log(Level.SEVERE, "Failed to add item to queue", e); - Platform.runLater(() -> { - showValidationError("Failed to add item: " + e.getMessage()); - }); - } - } - - private void showValidationError(String message) { - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setTitle("Validation Error"); - alert.setHeaderText(null); - alert.setContentText(message); - alert.showAndWait(); - } - - private void saveItem() { - if (!isEditMode || currentItem == null) { - return; - } - - if (!areParametersValid()) { - showValidationError("Some parameters have invalid values. Please check the red fields."); - return; - } - - try { - String selectedName = choiceBox.getSelectionModel().getSelectedItem(); - if (selectedName == null) { - return; - } - - String itemType = planRadBtn.isSelected() ? "plan" : "instruction"; - String itemUid = currentItem.itemUid(); - String user = currentItem.user(); - String userGroup = currentItem.userGroup(); - Map result = currentItem.result(); - - new Thread(() -> { - try { - // Use Python-based parameter conversion (in background thread to avoid blocking UI) - Map kwargs = buildKwargsWithPython(); - - QueueItem updatedItem = new QueueItem( - itemType, - selectedName, - List.of(), - kwargs, - itemUid, - user, - userGroup, - result - ); - - var response = svc.queueItemUpdate(updatedItem); - Platform.runLater(() -> { - if (response.success()) { - // Notify viewer that the item was updated - ItemUpdateEvent.getInstance().notifyItemUpdated(updatedItem); - - // Clear parameters but preserve radio button selection - parameterRows.clear(); - choiceBox.getSelectionModel().clearSelection(); - // Switch to view tab - TabSwitchEvent.getInstance().switchToTab("Plan Viewer"); - exitEditMode(); - showItemPreview(); - } else { - showValidationError("Failed to save item: " + response.msg()); - } - }); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to save item", e); - Platform.runLater(() -> { - String errorMsg = e.getMessage(); - if (errorMsg == null || errorMsg.isEmpty()) { - errorMsg = e.getClass().getSimpleName(); - } - showValidationError("Failed to save item: " + errorMsg); - }); - } - }).start(); - - } catch (Exception e) { - logger.log(Level.SEVERE, "Failed to save item", e); - Platform.runLater(() -> { - showValidationError("Failed to save item: " + e.getMessage()); - }); - } - } - - private void resetForm() { - parameterRows.clear(); - choiceBox.getSelectionModel().clearSelection(); - planRadBtn.setSelected(true); - populateChoiceBox(true); - } - - private void resetParametersToDefaults() { - if (isEditMode && currentItem != null) { - // Reset to original values from when editing started - for (ParameterRow row : parameterRows) { - String paramName = row.getName(); - if (originalParameterValues.containsKey(paramName)) { - // Restore original value - Object originalValue = originalParameterValues.get(paramName); - row.setValue(originalValue != null ? PythonParameterConverter.toPythonRepr(originalValue) : ""); - row.setEnabled(true); - } else { - // Parameter was not in original item, reset to default - // NOTE: Use simple string conversion - never call Python converter from JavaFX thread - Object defaultValue = row.getDefaultValue(); - row.setValue(defaultValue != null ? String.valueOf(defaultValue) : ""); - row.setEnabled(!row.isOptional()); - } - } - } else { - // Reset to default values for new items - // NOTE: Use simple string conversion - never call Python converter from JavaFX thread - for (ParameterRow row : parameterRows) { - Object defaultValue = row.getDefaultValue(); - row.setValue(defaultValue != null ? String.valueOf(defaultValue) : ""); - // Reset enabled state based on whether parameter is optional - row.setEnabled(!row.isOptional()); - } - } - // Keep edit mode active after reset - don't exit edit mode - updateButtonStates(); - } - - private void cancelEdit() { - if (isEditMode) { - // Reset to original state and exit edit mode - parameterRows.clear(); - isEditMode = false; - currentItem = null; - currentItemSource = ""; - originalParameterValues.clear(); - choiceBox.getSelectionModel().clearSelection(); - planRadBtn.setSelected(true); - populateChoiceBox(true); - updateButtonStates(); - showItemPreview(); - } else { - resetForm(); - } - } - - public void editItem(QueueItem item) { - currentItem = item; - currentItemSource = "QUEUE ITEM"; - isEditMode = true; - - if ("plan".equals(item.itemType())) { - planRadBtn.setSelected(true); - populateChoiceBox(true); - } else { - instrRadBtn.setSelected(true); - populateChoiceBox(false); - } - - choiceBox.getSelectionModel().select(item.name()); - - // Explicitly load parameters for the selected item since we're in edit mode - loadParametersForSelection(item.name()); - - // Update button states instead of visibility - updateButtonStates(); - } - - private void switchToEditingMode() { - if (!isEditMode) { - isEditMode = true; - currentItemSource = "NEW ITEM"; - updateButtonStates(); - } - } - - private void exitEditMode() { - isEditMode = false; - currentItem = null; - currentItemSource = ""; - originalParameterValues.clear(); - // Update button states instead of visibility - updateButtonStates(); - resetForm(); - } - - private void openBatchUpload() { - BatchUploadDialog dialog = new BatchUploadDialog(table.getScene().getWindow()); - Optional result = dialog.showAndWait(); - - if (result.isPresent()) { - BatchUploadDialog.Result uploadResult = result.get(); - processBatchFile(uploadResult.getFilePath(), uploadResult.getFileType()); - } - } - - private void processBatchFile(String filePath, String fileType) { - // Capture insert position before starting background thread - String afterUid = QueueItemSelectionEvent.getInstance().getLastSelectedUid(); - - new Thread(() -> { - try { - List items = new ArrayList<>(); - java.io.File file = new java.io.File(filePath); - - if ("csv".equals(fileType) || filePath.toLowerCase().endsWith(".csv")) { - items = parseCSVFile(file); - } else if ("xls".equals(fileType) || filePath.toLowerCase().endsWith(".xls")) { - items = parseExcelFile(file); - } - - if (!items.isEmpty()) { - final int itemCount = items.size(); - QueueItemAddBatch batchRequest = new QueueItemAddBatch(items, currentUser, currentUserGroup, afterUid); - List newUids = svc.addBatchGetUids(batchRequest); - QueueItemSelectionEvent.getInstance().requestSelectByUids(newUids); - - Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle("Batch Upload Success"); - alert.setHeaderText(null); - alert.setContentText("Successfully added " + itemCount + " items to queue."); - alert.showAndWait(); - }); - } else { - Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.WARNING); - alert.setTitle("No Items Found"); - alert.setHeaderText(null); - alert.setContentText("No valid items found in the file."); - alert.showAndWait(); - }); - } - - } catch (Exception e) { - logger.log(Level.WARNING, "Batch file processing error", e); - Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText(null); - alert.setContentText("Failed to process batch file: " + e.getMessage()); - alert.showAndWait(); - }); - } - }).start(); - } - - private List parseCSVFile(java.io.File file) throws Exception { - List items = new ArrayList<>(); - - try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(file))) { - String line = reader.readLine(); - if (line == null) return items; - - // Parse header - String[] headers = line.split(","); - - // Read data rows - while ((line = reader.readLine()) != null) { - String[] values = line.split(","); - if (values.length >= 2) { - String itemType = values[0].trim(); - String planName = values[1].trim(); - - Map kwargs = new HashMap<>(); - - // Parse additional parameters - for (int i = 2; i < Math.min(values.length, headers.length); i++) { - String paramName = headers[i].trim(); - String paramValue = values[i].trim(); - - if (!paramValue.isEmpty()) { - try { - // Try to parse as number or boolean - if (paramValue.equals("true") || paramValue.equals("false")) { - kwargs.put(paramName, Boolean.parseBoolean(paramValue)); - } else if (paramValue.matches("-?\\d+")) { - kwargs.put(paramName, Integer.parseInt(paramValue)); - } else if (paramValue.matches("-?\\d+\\.\\d+")) { - kwargs.put(paramName, Double.parseDouble(paramValue)); - } else { - kwargs.put(paramName, paramValue); - } - } catch (Exception e) { - kwargs.put(paramName, paramValue); - } - } - } - - QueueItem item = new QueueItem( - itemType.isEmpty() ? "plan" : itemType, - planName, - List.of(), - kwargs, - null, - currentUser, - currentUserGroup, - null - ); - items.add(item); - } - } - } - - return items; - } - - private List parseExcelFile(java.io.File file) throws Exception { - List items = new ArrayList<>(); - - try (java.io.FileInputStream fis = new java.io.FileInputStream(file); - Workbook workbook = new HSSFWorkbook(fis)) { - - Sheet sheet = workbook.getSheetAt(0); // Use first sheet - - if (sheet.getPhysicalNumberOfRows() == 0) { - return items; - } - - // Parse header row - Row headerRow = sheet.getRow(0); - if (headerRow == null) { - return items; - } - - List headers = new ArrayList<>(); - for (int i = 0; i < headerRow.getLastCellNum(); i++) { - Cell cell = headerRow.getCell(i); - String header = getCellValueAsString(cell); - headers.add(header != null ? header.trim() : ""); - } - - // Parse data rows - for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) { - Row row = sheet.getRow(rowNum); - if (row == null) continue; - - // Skip empty rows - boolean hasData = false; - for (int i = 0; i < Math.min(2, headers.size()); i++) { - Cell cell = row.getCell(i); - if (cell != null && getCellValueAsString(cell) != null && !getCellValueAsString(cell).trim().isEmpty()) { - hasData = true; - break; - } - } - if (!hasData) continue; - - String itemType = ""; - String planName = ""; - - if (headers.size() >= 1) { - Cell cell = row.getCell(0); - itemType = getCellValueAsString(cell); - itemType = itemType != null ? itemType.trim() : ""; - } - - if (headers.size() >= 2) { - Cell cell = row.getCell(1); - planName = getCellValueAsString(cell); - planName = planName != null ? planName.trim() : ""; - } - - if (planName.isEmpty()) continue; - - Map kwargs = new HashMap<>(); - - // Parse additional parameters - for (int i = 2; i < Math.min(headers.size(), row.getLastCellNum()); i++) { - String paramName = headers.get(i).trim(); - if (paramName.isEmpty()) continue; - - Cell cell = row.getCell(i); - Object paramValue = getCellValueAsObject(cell); - - if (paramValue != null) { - kwargs.put(paramName, paramValue); - } - } - - QueueItem item = new QueueItem( - itemType.isEmpty() ? "plan" : itemType, - planName, - List.of(), - kwargs, - null, - currentUser, - currentUserGroup, - null - ); - items.add(item); - } - } - - return items; - } - - private String getCellValueAsString(Cell cell) { - if (cell == null) return null; - - switch (cell.getCellType()) { - case STRING: - return cell.getStringCellValue(); - case NUMERIC: - if (DateUtil.isCellDateFormatted(cell)) { - return cell.getDateCellValue().toString(); - } else { - double numValue = cell.getNumericCellValue(); - if (numValue == Math.floor(numValue)) { - return String.valueOf((long) numValue); - } else { - return String.valueOf(numValue); - } - } - case BOOLEAN: - return String.valueOf(cell.getBooleanCellValue()); - case FORMULA: - try { - return cell.getStringCellValue(); - } catch (Exception e) { - try { - double numValue = cell.getNumericCellValue(); - if (numValue == Math.floor(numValue)) { - return String.valueOf((long) numValue); - } else { - return String.valueOf(numValue); - } - } catch (Exception e2) { - return null; - } - } - case BLANK: - case _NONE: - default: - return null; - } - } - - private Object getCellValueAsObject(Cell cell) { - if (cell == null) return null; - - switch (cell.getCellType()) { - case STRING: - String strValue = cell.getStringCellValue().trim(); - if (strValue.isEmpty()) return null; - - // Try to parse as boolean - if ("true".equalsIgnoreCase(strValue) || "false".equalsIgnoreCase(strValue)) { - return Boolean.parseBoolean(strValue); - } - - // Try to parse as number - try { - if (strValue.contains(".")) { - return Double.parseDouble(strValue); - } else { - return Long.parseLong(strValue); - } - } catch (NumberFormatException e) { - return strValue; - } - - case NUMERIC: - if (DateUtil.isCellDateFormatted(cell)) { - return cell.getDateCellValue(); - } else { - double numValue = cell.getNumericCellValue(); - if (numValue == Math.floor(numValue)) { - return (long) numValue; - } else { - return numValue; - } - } - case BOOLEAN: - return cell.getBooleanCellValue(); - case FORMULA: - try { - // Handle formula cells by trying to get the cached result - CellType cachedType = cell.getCachedFormulaResultType(); - switch (cachedType) { - case STRING: - return cell.getStringCellValue(); - case NUMERIC: - double numValue = cell.getNumericCellValue(); - if (numValue == Math.floor(numValue)) { - return (long) numValue; - } else { - return numValue; - } - case BOOLEAN: - return cell.getBooleanCellValue(); - default: - return null; - } - } catch (Exception e) { - return null; - } - case BLANK: - case _NONE: - default: - return null; - } - } - - private void showItemPreview() { - String selectedItem = choiceBox.getSelectionModel().getSelectedItem(); - if (selectedItem != null) { - loadParametersForSelection(selectedItem); - } - } - - private void setChoiceBoxTooltip(String itemName) { - Map itemInfo; - if (planRadBtn.isSelected()) { - itemInfo = allowedPlans.get(itemName); - } else { - itemInfo = allowedInstructions.get(itemName); - } - - if (itemInfo != null) { - String description = (String) itemInfo.get("description"); - if (description != null && !description.isEmpty()) { - Tooltip tooltip = new Tooltip(description); - tooltip.setWrapText(true); - tooltip.setMaxWidth(400); - choiceBox.setTooltip(tooltip); - } else { - choiceBox.setTooltip(new Tooltip("Description for '" + itemName + "' was not found...")); - } - } - } - - private static class BatchUploadDialog extends Dialog { - - public static class Result { - private final String filePath; - private final String fileType; - - public Result(String filePath, String fileType) { - this.filePath = filePath; - this.fileType = fileType; - } - - public String getFilePath() { return filePath; } - public String getFileType() { return fileType; } - } - - private TextField filePathField; - private ComboBox fileTypeCombo; - private Button browseButton; - private String selectedFilePath; - - public BatchUploadDialog(javafx.stage.Window owner) { - initOwner(owner); - setTitle("Batch Upload"); - setHeaderText("Load Plans from Spreadsheet"); - - // Create content - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - grid.setPadding(new Insets(20, 150, 10, 10)); - - // File selection - browseButton = new Button("..."); - browseButton.setOnAction(e -> selectFile()); - - filePathField = new TextField(); - filePathField.setEditable(false); - filePathField.setPrefWidth(300); - - // File type selection - fileTypeCombo = new ComboBox<>(); - fileTypeCombo.getItems().addAll("xls", "csv"); - fileTypeCombo.setValue("xls"); - - grid.add(browseButton, 0, 0); - grid.add(filePathField, 1, 0); - grid.add(new Label("Spreadsheet Type:"), 0, 1); - grid.add(fileTypeCombo, 1, 1); - - getDialogPane().setContent(grid); - - // Add buttons - getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - - // Initially disable OK button - getDialogPane().lookupButton(ButtonType.OK).setDisable(true); - - // Enable OK button when file is selected - filePathField.textProperty().addListener((obs, oldVal, newVal) -> { - getDialogPane().lookupButton(ButtonType.OK).setDisable(newVal == null || newVal.trim().isEmpty()); - }); - - // Result converter - setResultConverter(dialogButton -> { - if (dialogButton == ButtonType.OK && selectedFilePath != null) { - return new Result(selectedFilePath, fileTypeCombo.getValue()); - } - return null; - }); - } - - private void selectFile() { - javafx.stage.FileChooser fileChooser = new javafx.stage.FileChooser(); - fileChooser.setTitle("Select Spreadsheet File"); - fileChooser.getExtensionFilters().addAll( - new javafx.stage.FileChooser.ExtensionFilter("Excel Legacy Files (*.xls)", "*.xls"), - new javafx.stage.FileChooser.ExtensionFilter("CSV Files (*.csv)", "*.csv"), - new javafx.stage.FileChooser.ExtensionFilter("All Files", "*.*") - ); - - java.io.File file = fileChooser.showOpenDialog(getDialogPane().getScene().getWindow()); - if (file != null) { - selectedFilePath = file.getAbsolutePath(); - filePathField.setText(selectedFilePath); - - // Auto-detect file type based on extension - String fileName = file.getName().toLowerCase(); - if (fileName.endsWith(".xls")) { - fileTypeCombo.setValue("xls"); - } else if (fileName.endsWith(".csv")) { - fileTypeCombo.setValue("csv"); - } - } - } - } - -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java deleted file mode 100644 index 10f7ed7ec8..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java +++ /dev/null @@ -1,517 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import org.phoebus.applications.queueserver.api.HistoryGetPayload; -import org.phoebus.applications.queueserver.api.QueueItem; -import org.phoebus.applications.queueserver.api.QueueItemAddBatch; -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.PlansCache; -import org.phoebus.applications.queueserver.util.PythonParameterConverter; -import org.phoebus.applications.queueserver.util.QueueItemSelectionEvent; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.application.Platform; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.ReadOnlyStringWrapper; -import javafx.beans.value.ChangeListener; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.*; -import javafx.scene.text.Text; -import javafx.stage.FileChooser; - -import java.io.BufferedWriter; -import java.io.File; -import java.net.URL; -import java.nio.file.Files; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public final class RePlanHistoryController implements Initializable { - - @FXML private TableView table; - @FXML private TableColumn idxCol; - @FXML private TableColumn typeCol,nameCol,statusCol,paramCol,userCol,grpCol; - @FXML private Button copyBtn, deselectBtn, clearBtn; - @FXML private SplitMenuButton exportBtn; - @FXML private MenuItem exportTxtItem, exportJsonItem, exportYamlItem; - - - private final RunEngineService svc = new RunEngineService(); - private final ObservableListrows = FXCollections.observableArrayList(); - private final Map uid2item = new HashMap<>(); - private final Map> allowedPlans = new HashMap<>(); - private final Map> allowedInstructions = new HashMap<>(); - - private List stickySel = List.of(); - private boolean ignoreSel = false; - private static final Logger logger = - Logger.getLogger(RePlanHistoryController.class.getPackageName()); - - private final boolean viewOnly; - - // Drag-to-select state - private int dragStartRow = -1; - - public RePlanHistoryController() { - this(false); // default to editable - } - - public RePlanHistoryController(boolean viewOnly) { - this.viewOnly = viewOnly; - } - - @Override public void initialize(URL u, ResourceBundle rb) { - - table.setItems(rows); - table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - idxCol.setCellValueFactory(c -> new ReadOnlyObjectWrapper<>(rows.indexOf(c.getValue())+1)); - idxCol.setSortable(false); - - typeCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(firstLetter(c.getValue().itemType()))); - typeCol.setStyle("-fx-alignment:CENTER;"); - nameCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().name())); - statusCol.setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().status())); - paramCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().params())); - userCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().user())); - grpCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().group())); - - table.getSelectionModel().getSelectedIndices() - .addListener((ListChangeListener) c -> updateButtonStates()); - - deselectBtn.setOnAction(e -> { - table.getSelectionModel().clearSelection(); - stickySel = List.of(); - }); - - if (viewOnly) { - copyBtn.setDisable(true); - clearBtn.setDisable(true); - } else { - hookButtons(); - updateButtonStates(); - } - - hookExportButtons(); - - table.getSelectionModel().getSelectedIndices() - .addListener((ListChangeListener) c -> { - if (!ignoreSel) stickySel = - List.copyOf(table.getSelectionModel().getSelectedIndices()); - }); - - initializeAllowedInstructions(); - - if (PlansCache.isLoaded()) { - allowedPlans.clear(); - allowedPlans.putAll(PlansCache.get()); - } - PlansCache.addListener((o2, oldP, newP) -> { - Platform.runLater(() -> { - allowedPlans.clear(); - if (newP != null) allowedPlans.putAll(newP); - }); - }); - - ChangeListener l = - (o,oldV,nv) -> { - // Run refresh in background thread to avoid blocking UI - new Thread(() -> refresh(nv)).start(); - }; - StatusBus.addListener(l); - - // Add drag-to-select functionality - setupDragSelection(); - - // Run initial refresh in background thread - new Thread(() -> refresh(StatusBus.latest().get())).start(); - } - - /** - * Sets up drag-to-select functionality for the table. - * Users can click and drag to select multiple rows. - */ - private void setupDragSelection() { - table.setOnMousePressed(event -> { - int index = getRowIndexAt(event.getY()); - if (index >= 0 && index < rows.size()) { - dragStartRow = index; - // Don't consume - let normal click work - } else { - dragStartRow = -1; - } - }); - - table.setOnMouseDragged(event -> { - if (dragStartRow >= 0) { - int currentIndex = getRowIndexAt(event.getY()); - if (currentIndex >= 0 && currentIndex < rows.size()) { - selectRange(dragStartRow, currentIndex); - } - event.consume(); // Prevent default drag behavior - } - }); - - table.setOnMouseReleased(event -> { - dragStartRow = -1; - }); - } - - /** - * Gets the row index at the specified Y coordinate relative to the table. - */ - private int getRowIndexAt(double y) { - // Get the fixed cell size or estimate - if (rows.isEmpty()) return -1; - - // Look through visible rows - for (javafx.scene.Node node : table.lookupAll(".table-row-cell")) { - if (node instanceof TableRow) { - @SuppressWarnings("unchecked") - TableRow row = (TableRow) node; - - // Convert to table's coordinate space - javafx.geometry.Bounds boundsInTable = table.sceneToLocal( - row.localToScene(row.getBoundsInLocal()) - ); - - if (y >= boundsInTable.getMinY() && y <= boundsInTable.getMaxY()) { - return row.getIndex(); - } - } - } - return -1; - } - - /** - * Selects all rows between start and end indices (inclusive). - */ - private void selectRange(int start, int end) { - table.getSelectionModel().clearSelection(); - int from = Math.min(start, end); - int to = Math.max(start, end); - for (int i = from; i <= to; i++) { - table.getSelectionModel().select(i); - } - } - - private void refresh(StatusResponse st) { - - if (st == null) { - // Don't clear history - keep last data visible for users - // Just update button states (will be disabled via StatusBus) - Platform.runLater(this::updateButtonStates); - return; - } - - try { - // Blocking HTTP call - runs on background thread - HistoryGetPayload hp = svc.historyGetTyped(); // typed DTO - List items = hp.items(); - - if (items == null) { - Platform.runLater(() -> { - rows.clear(); - uid2item.clear(); - updateButtonStates(); - }); - return; - } - - // Build rows in background thread (expensive fmtParams processing) - List newRows = new ArrayList<>(items.size()); - Map newUidMap = new HashMap<>(); - for (QueueItem qi : items) { - newRows.add(new Row( - qi.itemUid(), - qi.itemType(), - qi.name(), - exitStatus(qi), - fmtParams(qi), - qi.user(), - qi.userGroup())); - newUidMap.put(qi.itemUid(), qi); - } - - // UI updates on FX thread - Platform.runLater(() -> { - ignoreSel = true; - rows.setAll(newRows); - uid2item.clear(); - uid2item.putAll(newUidMap); - autoResizeColumns(); - ignoreSel = false; - restoreSelection(stickySel); - updateButtonStates(); - }); - } catch (Exception ex) { - logger.log(Level.FINE, "History refresh failed: " + ex.getMessage()); - } - } - - private void hookButtons() { - copyBtn .setOnAction(e -> copySelectedToQueue()); - deselectBtn.setOnAction(e -> table.getSelectionModel().clearSelection()); - clearBtn .setOnAction(e -> clearHistory()); - - table.getSelectionModel().getSelectedIndices() - .addListener((ListChangeListener) c -> updateButtonStates()); - } - - private void hookExportButtons() { - exportTxtItem.setOnAction(e -> exportHistory(PlanHistorySaver.Format.TXT, "txt")); - exportJsonItem.setOnAction(e -> exportHistory(PlanHistorySaver.Format.JSON, "json")); - exportYamlItem.setOnAction(e -> exportHistory(PlanHistorySaver.Format.YAML, "yaml")); - - exportBtn.setOnAction(e -> exportHistory(PlanHistorySaver.Format.TXT, "txt")); - } - - private void copySelectedToQueue() { - var sel = table.getSelectionModel().getSelectedIndices(); - if (sel.isEmpty()) return; - - // Capture insert position before starting background thread - String afterUid = QueueItemSelectionEvent.getInstance().getLastSelectedUid(); - - List clones = sel.stream() - .map(rows::get) - .map(r -> uid2item.get(r.uid)) - .filter(Objects::nonNull) - .map(q -> new QueueItem( - q.itemType(), - q.name(), - q.args(), - q.kwargs(), - null, - q.user(), - q.userGroup(), - q.result() - )) - .toList(); - - new Thread(() -> { - try { - QueueItemAddBatch req = - new QueueItemAddBatch(clones, "GUI Client", "primary", afterUid); - List newUids = svc.addBatchGetUids(req); - QueueItemSelectionEvent.getInstance().requestSelectByUids(newUids); - } catch (Exception ex) { - logger.log(Level.WARNING, "Copy-to-Queue failed", ex); - } - }).start(); - } - - private void clearHistory() { - try { svc.historyClear(); } - catch (Exception ex) { logger.log(Level.WARNING, "Clear-history failed", ex); } - } - - private void exportHistory(PlanHistorySaver.Format fmt, String ext) { - FileChooser fc = new FileChooser(); - fc.getExtensionFilters().add(new FileChooser.ExtensionFilter(ext.toUpperCase(), "."+ext)); - fc.setInitialFileName("plan_history." + ext); - File f = fc.showSaveDialog(table.getScene().getWindow()); - if (f == null) return; - - Platform.runLater(() -> { - try { - HistoryGetPayload hp = svc.historyGetTyped(); - List items = hp.items(); - PlanHistorySaver.save(items, f, fmt); - logger.log(Level.FINE, () -> "Exported plan history → " + f); - } catch (Exception e) { - logger.log(Level.WARNING, "Export history failed", e); - } - }); - } - - private void updateButtonStates() { - StatusResponse status = StatusBus.latest().get(); - boolean connected = status != null; - boolean hasSel = !table.getSelectionModel().getSelectedIndices().isEmpty(); - - if (viewOnly) { - copyBtn.setDisable(true); - clearBtn.setDisable(true); - deselectBtn.setDisable(!hasSel); - } else { - // Only require server connection (not environment open) - copyBtn.setDisable(!(connected && hasSel)); - clearBtn.setDisable(!(connected && !rows.isEmpty())); - // Deselect and export always enabled when there's data - deselectBtn.setDisable(!hasSel); - } - } - - - private void autoResizeColumns() { - table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY); - for (TableColumn col : table.getColumns()) { - Text probe = new Text(col.getText()); - double max = probe.getLayoutBounds().getWidth(); - for (int i=0;i idx) { - if (idx.isEmpty()) return; - var sm = table.getSelectionModel(); - var fm = table.getFocusModel(); - sm.clearSelection(); - int first = -1; - for (Integer i : idx) { - if (i>=0 && i queueStopInstr = new HashMap<>(); - queueStopInstr.put("name", "queue_stop"); - queueStopInstr.put("description", "Stop execution of the queue."); - queueStopInstr.put("parameters", List.of()); - allowedInstructions.put("queue_stop", queueStopInstr); - } - - @SuppressWarnings("unchecked") - private String fmtParams(QueueItem q){ - String a = Optional.ofNullable(q.args()).orElse(List.of()) - .stream().map(PythonParameterConverter::toPythonRepr) - .collect(Collectors.joining(", ")); - - Map itemInfo = null; - if ("plan".equals(q.itemType())) { - itemInfo = allowedPlans.get(q.name()); - } else if ("instruction".equals(q.itemType())) { - itemInfo = allowedInstructions.get(q.name()); - } - - String k; - if (itemInfo != null) { - List> parameters = - (List>) itemInfo.get("parameters"); - if (parameters != null) { - Map kwargs = Optional.ofNullable(q.kwargs()).orElse(Map.of()); - k = parameters.stream() - .map(p -> (String) p.get("name")) - .filter(kwargs::containsKey) - .map(name -> name + ": " + PythonParameterConverter.toPythonRepr(kwargs.get(name))) - .collect(Collectors.joining(", ")); - } else { - k = formatKwargsDefault(q.kwargs()); - } - } else { - k = formatKwargsDefault(q.kwargs()); - } - - return Stream.of(a,k).filter(s->!s.isEmpty()) - .collect(Collectors.joining(", ")); - } - - private static String formatKwargsDefault(Map kwargs) { - return Optional.ofNullable(kwargs).orElse(Map.of()) - .entrySet().stream() - .map(e -> e.getKey() + ": " + PythonParameterConverter.toPythonRepr(e.getValue())) - .collect(Collectors.joining(", ")); - } - - private static String exitStatus(QueueItem q){ - Map res = q.result(); - - return res == null ? "" - : String.valueOf(res.getOrDefault("exit_status","")); - } - - private record Row( - String uid, - String itemType, - String name, - String status, - String params, - String user, - String group) {} - - private static final class PlanHistorySaver { - - private static final ObjectMapper JSON = - (ObjectMapper) new ObjectMapper() - .writerWithDefaultPrettyPrinter() - .withDefaultPrettyPrinter() - .getFactory() - .getCodec(); - - private static final DateTimeFormatter TS = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS") - .withZone(ZoneId.systemDefault()); - - private PlanHistorySaver() {} - enum Format {TXT, JSON, YAML} - - static void save(List history, File file, Format fmt) throws Exception { - switch (fmt) { - case TXT -> writeTxt(history, file); - case JSON -> JSON.writerWithDefaultPrettyPrinter().writeValue(file, history); - case YAML -> new ObjectMapper(new YAMLFactory()) - .writerWithDefaultPrettyPrinter() - .writeValue(file, history); - } - } - - private static void writeTxt(List h, File f) throws Exception { - try (BufferedWriter w = Files.newBufferedWriter(f.toPath())) { - - int idx = 0; - for (QueueItem qi : h) { - - Map res = qi.result(); - double t0 = res == null ? 0 : ((Number)res.getOrDefault("time_start",0)).doubleValue(); - double t1 = res == null ? 0 : ((Number)res.getOrDefault("time_stop" ,0)).doubleValue(); - - w.write("=".repeat(80)); w.newLine(); - - String hdr = "PLAN " + (++idx); - if (t0>0) { - hdr += ": " + TS.format(Instant.ofEpochMilli((long)(t0*1_000))); - if (t1>0) - hdr += " - " + TS.format(Instant.ofEpochMilli((long)(t1*1_000))); - } - w.write(center(hdr,80)); w.newLine(); - w.write("=".repeat(80)); w.newLine(); - - String pretty = JSON.writerWithDefaultPrettyPrinter() - .writeValueAsString(JSON.convertValue(qi, Map.class)); - w.write(pretty); w.newLine(); w.newLine(); - } - } - } - - private static String center(String text, int width) { - int pad = Math.max(0, (width - text.length()) / 2); - return " ".repeat(pad) + text; - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java deleted file mode 100644 index 8d2fd7ab5f..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.view.TabSwitchEvent; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; - -import java.net.URL; -import java.util.ResourceBundle; - -public class RePlanManagerController implements Initializable { - - @FXML private TabPane tabPane; - @FXML private Tab planViewerTab; - @FXML private Tab planEditorTab; - - @Override - public void initialize(URL location, ResourceBundle resources) { - TabSwitchEvent.getInstance().addListener(this::switchToTab); - } - - private void switchToTab(String tabName) { - if (tabPane == null) return; - - switch (tabName) { - case "Plan Editor" -> tabPane.getSelectionModel().select(planEditorTab); - case "Plan Viewer" -> tabPane.getSelectionModel().select(planViewerTab); - } - } - - public String getCurrentTabName() { - if (tabPane == null || tabPane.getSelectionModel().getSelectedItem() == null) { - return null; - } - return tabPane.getSelectionModel().getSelectedItem().getText(); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java deleted file mode 100644 index 6549edd67a..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java +++ /dev/null @@ -1,541 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.api.*; -import org.phoebus.applications.queueserver.api.QueueGetPayload; -import org.phoebus.applications.queueserver.api.QueueItem; -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.PlansCache; -import org.phoebus.applications.queueserver.util.StatusBus; -import org.phoebus.applications.queueserver.util.QueueItemSelectionEvent; -import org.phoebus.applications.queueserver.util.PythonParameterConverter; -import javafx.application.Platform; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.ReadOnlyStringWrapper; -import javafx.beans.value.ChangeListener; -import javafx.collections.*; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.*; -import javafx.scene.input.DataFormat; -import javafx.scene.input.Dragboard; -import javafx.scene.input.TransferMode; -import javafx.scene.text.Text; - -import java.net.URL; -import java.util.*; -import java.util.concurrent.atomic.AtomicLong; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public final class RePlanQueueController implements Initializable { - - @FXML private TableView table; - @FXML private TableColumn idxCol; - @FXML private TableColumn typeCol, nameCol, paramCol, userCol, grpCol; - @FXML private Button upBtn, downBtn, topBtn, bottomBtn, - deleteBtn, duplicateBtn, clearBtn, deselectBtn; - @FXML private ToggleButton loopBtn; - - - private final RunEngineService svc = new RunEngineService(); - private final ObservableList rows = FXCollections.observableArrayList(); - private final Map uid2item = new HashMap<>(); - private final Map> allowedPlans = new HashMap<>(); - private final Map> allowedInstructions = new HashMap<>(); - private List stickySel = List.of(); // last user selection - private boolean ignoreSticky= false; // guard while we rebuild - private final AtomicLong refreshSeq = new AtomicLong(); // discard stale bg refreshes - - private static final Logger logger = - Logger.getLogger(RePlanQueueController.class.getPackageName()); - - private final boolean viewOnly; - - public RePlanQueueController() { - this(false); // default to editable - } - - public RePlanQueueController(boolean viewOnly) { - this.viewOnly = viewOnly; - } - - @Override public void initialize(URL url, ResourceBundle rb) { - - table.setItems(rows); - table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - idxCol .setCellValueFactory(c -> - new ReadOnlyObjectWrapper<>(rows.indexOf(c.getValue()) + 1)); - idxCol.setSortable(false); - - typeCol.setCellValueFactory(c -> - new ReadOnlyStringWrapper(firstLetter(c.getValue().itemType()))); - typeCol.setStyle("-fx-alignment:CENTER;"); - - nameCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().name())); - paramCol.setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().params())); - userCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().user())); - grpCol .setCellValueFactory(c -> new ReadOnlyStringWrapper(c.getValue().group())); - - table.getSelectionModel().getSelectedIndices() - .addListener((ListChangeListener) c -> updateButtonStates()); - - deselectBtn.setOnAction(e -> { - table.getSelectionModel().clearSelection(); - stickySel = List.of(); - }); - - if (viewOnly) { - upBtn.setDisable(true); - downBtn.setDisable(true); - topBtn.setDisable(true); - bottomBtn.setDisable(true); - deleteBtn.setDisable(true); - duplicateBtn.setDisable(true); - clearBtn.setDisable(true); - loopBtn.setDisable(true); - } else { - enableRowDragAndDrop(); - hookButtons(); - updateButtonStates(); - } - - table.getSelectionModel().getSelectedItems() - .addListener((ListChangeListener) c -> { - if (!ignoreSticky) { - stickySel = selectedUids(); - // Notify plan viewer of selection change - notifySelectionChange(); - } - }); - - initializeAllowedInstructions(); - - // Use shared plans cache - copy to local map when loaded - if (PlansCache.isLoaded()) { - allowedPlans.clear(); - allowedPlans.putAll(PlansCache.get()); - } - - // Listen for plans cache updates - PlansCache.addListener((o, oldV, newV) -> { - Platform.runLater(() -> { - allowedPlans.clear(); - if (newV != null) { - allowedPlans.putAll(newV); - } - }); - }); - - // Listen for status changes to refresh queue - ChangeListener poll = - (o,oldV,newV) -> { - long seq = refreshSeq.incrementAndGet(); - new Thread(() -> bgRefresh(newV, seq)).start(); - }; - StatusBus.addListener(poll); - - // Initial refresh on background thread - long seq = refreshSeq.incrementAndGet(); - new Thread(() -> bgRefresh(StatusBus.latest().get(), seq)).start(); - } - - private void initializeAllowedInstructions() { - Map queueStopInstr = new HashMap<>(); - queueStopInstr.put("name", "queue_stop"); - queueStopInstr.put("description", "Stop execution of the queue."); - queueStopInstr.put("parameters", List.of()); - allowedInstructions.put("queue_stop", queueStopInstr); - } - - /** - * Background refresh: fetches queue data on a worker thread, then applies - * it on the FX thread. Uses a generation counter ({@code seq}) so that - * out-of-order responses are silently discarded – only the latest data is - * ever applied to the UI. - */ - private void bgRefresh(StatusResponse st, long seq) { - if (st == null) { - Platform.runLater(() -> { - if (seq < refreshSeq.get()) return; - updateButtonStates(); - }); - return; - } - try { - QueueGetPayload qp = svc.queueGetTyped(); - Platform.runLater(() -> { - if (seq < refreshSeq.get()) return; // stale – discard - applyQueueData(qp, List.of(), st); - }); - } catch (Exception ex) { - logger.log(Level.FINE, "Queue refresh failed: " + ex.getMessage()); - } - } - - /** Applies fetched queue data to the UI. Must be called on the FX thread. */ - private void applyQueueData(QueueGetPayload qp, Collection explicitFocus, - StatusResponse st) { - QueueItemSelectionEvent selEvent = QueueItemSelectionEvent.getInstance(); - - ignoreSticky = true; - rebuildRows(qp.queue()); - ignoreSticky = false; - - List focus; - List pendingUids = selEvent.getPendingSelectUids(); - if (pendingUids != null) { - // We have exact UIDs to select (from a successful add/copy response) - List found = pendingUids.stream() - .filter(uid2item::containsKey) - .toList(); - if (!found.isEmpty()) { - focus = found; - selEvent.clearPendingSelect(); - } else { - // UIDs not yet in this refresh's data – keep for next cycle - focus = explicitFocus.isEmpty() ? stickySel - : List.copyOf(explicitFocus); - } - } else if (!explicitFocus.isEmpty()) { - focus = List.copyOf(explicitFocus); - } else { - focus = stickySel; - } - - applyFocus(focus); - - // applyFocus suppresses intermediate notifications, so update - // stickySel and notify once here. - if (table.getSelectionModel().getSelectedItems().isEmpty()) { - stickySel = List.of(); - } else { - stickySel = selectedUids(); - } - notifySelectionChange(); - - loopBtn.setSelected(Optional.ofNullable(st.planQueueMode()) - .map(StatusResponse.PlanQueueMode::loop) - .orElse(false)); - } - - private void rebuildRows(List items) { - rows.clear(); uid2item.clear(); - if (items == null) { updateButtonStates(); return; } - - for (QueueItem q : items) { - rows.add(new Row(q.itemUid(), q.itemType(), q.name(), - fmtParams(q, q.itemType(), q.name()), q.user(), q.userGroup())); - uid2item.put(q.itemUid(), q); - } - updateButtonStates(); - autoResizeColumns(); - } - - - private void autoResizeColumns() { - table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY); - for (TableColumn col : table.getColumns()) { - - Text tmp = new Text(col.getText()); - double max = tmp.getLayoutBounds().getWidth(); - - for (int i = 0; i < rows.size(); i++) { - Object cell = col.getCellData(i); - if (cell != null) { - tmp = new Text(cell.toString()); - double w = tmp.getLayoutBounds().getWidth(); - if (w > max) max = w; - } - } - col.setPrefWidth(max + 14); - } - } - - private void applyFocus(Collection uids) { - if (uids.isEmpty()) return; - - var sm = table.getSelectionModel(); - var fm = table.getFocusModel(); - - // Suppress intermediate listener notifications during clear+select - ignoreSticky = true; - sm.clearSelection(); - - int first = -1; - for (int i = 0; i < rows.size(); i++) { - if (uids.contains(rows.get(i).uid())) { - sm.select(i); fm.focus(i); - if (first == -1) first = i; - } - } - ignoreSticky = false; - - if (first != -1) { - final int scrollIdx = first; - table.scrollTo(scrollIdx); - // Defer focus to next pulse for reliable visual highlight - Platform.runLater(() -> table.requestFocus()); - } - } - private void refreshLater(Collection focus) { - // Invalidate any pending background Platform.runLater blocks - refreshSeq.incrementAndGet(); - StatusResponse st = StatusBus.latest().get(); - if (st == null) { updateButtonStates(); return; } - try { - QueueGetPayload qp = svc.queueGetTyped(); - applyQueueData(qp, focus, st); - } catch (Exception ex) { - logger.log(Level.FINE, "Queue refresh (explicit) failed: " + ex.getMessage()); - } - } - - - private void hookButtons() { - upBtn .setOnAction(e -> moveRelative(-1)); - downBtn .setOnAction(e -> moveRelative(+1)); - topBtn .setOnAction(e -> moveAbsolute(0)); - bottomBtn .setOnAction(e -> moveAbsolute(rows.size()-1)); - deleteBtn .setOnAction(e -> deleteSelected()); - duplicateBtn.setOnAction(e -> duplicateSelected()); - clearBtn .setOnAction(e -> clearQueue()); - deselectBtn.setOnAction(e -> table.getSelectionModel().clearSelection()); - - loopBtn.selectedProperty().addListener((o,ov,nv) -> setLoopMode(nv)); - - table.getSelectionModel().getSelectedIndices() - .addListener((ListChangeListener) c -> updateButtonStates()); - } - - private void sendMove(List sel, String refUid, boolean before) throws Exception { - if (sel.size() == 1) svc.moveSingle(sel.get(0), refUid, before); - else svc.moveBatch (sel , refUid, before); - } - private void moveRelative(int delta) { - var selRows = table.getSelectionModel().getSelectedIndices(); - if (selRows.isEmpty()) return; - - int first = selRows.get(0), last = selRows.get(selRows.size()-1); - int ref = (delta<0)? first+delta : last+delta; - if (ref<0 || ref>=rows.size()) return; - - List focus = selectedUids(); - try { sendMove(focus, rows.get(ref).uid(), delta<0); refreshLater(focus); } - catch (Exception ex) { logger.log(Level.WARNING, "Move failed: "+ex.getMessage()); } - } - private void moveAbsolute(int targetRow) { - if (rows.isEmpty()) return; - targetRow = Math.max(0, Math.min(targetRow, rows.size()-1)); - - var selRows = table.getSelectionModel().getSelectedIndices(); - if (selRows.isEmpty()) return; - - boolean before = targetRow < selRows.get(0); - List focus = selectedUids(); - try { sendMove(focus, rows.get(targetRow).uid(), before); refreshLater(focus); } - catch (Exception ex) { logger.log(Level.WARNING, "Move-abs failed: "+ex.getMessage()); } - } - - private void deleteSelected() { - var selRows = table.getSelectionModel().getSelectedIndices(); - if (selRows.isEmpty()) return; - - int first = selRows.get(0), last = selRows.get(selRows.size()-1); - String nextFocus = (last+1 < rows.size()) ? rows.get(last+1).uid() - : (first>0 ? rows.get(first-1).uid() : null); - try { - svc.queueItemRemoveBatch(Map.of("uids", selectedUids())); - refreshLater(nextFocus==null? List.of() : List.of(nextFocus)); - } catch (Exception ex) { logger.log(Level.WARNING, "Delete failed: "+ex.getMessage()); } - } - - private void duplicateSelected() { - var selIdx = table.getSelectionModel().getSelectedIndices(); - if (selIdx.isEmpty()) return; - - // keep copy of the queue before duplication - Set before = new HashSet<>(uid2item.keySet()); - - // duplicate – top→bottom so indices don’t shift - selIdx.stream().sorted().forEach(idx -> { - QueueItem orig = uid2item.get(rows.get(idx).uid()); - if (orig == null) return; - try { svc.addAfter(orig, orig.itemUid()); } // server returns nothing we need - catch (Exception ex) { // log but continue - logger.log(Level.WARNING, "Duplicate RPC failed: "+ex.getMessage()); - } - }); - - try { - // Invalidate pending background refreshes so they don't overwrite our selection - refreshSeq.incrementAndGet(); - - QueueGetPayload qp = svc.queueGetTyped(); - List afterList = qp.queue().stream() - .map(QueueItem::itemUid).toList(); - List added = afterList.stream() - .filter(uid -> !before.contains(uid)) - .toList(); - - ignoreSticky = true; - rebuildRows(qp.queue()); - ignoreSticky = false; - - if (!added.isEmpty()) { - applyFocus(added); - stickySel = added; - notifySelectionChange(); - } - - } catch (Exception ex) { - logger.log(Level.WARNING, "Refresh after duplicate failed: "+ex.getMessage()); - } - } - - private void clearQueue() { try { svc.queueClear(); } - catch (Exception ex) { logger.log(Level.WARNING, "Clear failed: "+ex.getMessage()); } } - private void setLoopMode(boolean loop){ try { svc.queueModeSet(Map.of("loop",loop)); } - catch (Exception ex){ logger.log(Level.WARNING, "Loop-set failed: "+ex.getMessage()); } } - - private void updateButtonStates() { - StatusResponse status = StatusBus.latest().get(); - boolean connected = status != null; - - var sel = table.getSelectionModel().getSelectedIndices(); - boolean hasSel = !table.getSelectionModel().getSelectedIndices().isEmpty(); - boolean atTop = hasSel && table.getSelectionModel().getSelectedIndices().get(0) == 0; - boolean atBot = hasSel && table.getSelectionModel().getSelectedIndices().size() - 1== rows.size() - 1; - - if (viewOnly) { - upBtn.setDisable(true); - downBtn.setDisable(true); - topBtn.setDisable(true); - bottomBtn.setDisable(true); - deleteBtn.setDisable(true); - duplicateBtn.setDisable(true); - clearBtn.setDisable(true); - loopBtn.setDisable(true); - deselectBtn.setDisable(!hasSel); - } else { - // Only require server connection (not environment open) - upBtn.setDisable(!(connected && hasSel && !atTop)); - downBtn.setDisable(!(connected && hasSel && !atBot)); - topBtn.setDisable(upBtn.isDisable()); - bottomBtn.setDisable(downBtn.isDisable()); - deleteBtn.setDisable(!(connected && hasSel)); - duplicateBtn.setDisable(deleteBtn.isDisable()); - clearBtn.setDisable(!(connected && !rows.isEmpty())); - loopBtn.setDisable(!connected); - // Deselect always enabled when there's a selection - deselectBtn.setDisable(!hasSel); - } - } - - private void enableRowDragAndDrop() { - table.setRowFactory(tv -> { - TableRow row = new TableRow<>(); - row.setOnDragDetected(e -> { - if (!row.isEmpty()) { - Dragboard db = row.startDragAndDrop(TransferMode.MOVE); - db.setContent(Map.of(DataFormat.PLAIN_TEXT,"")); - table.getSelectionModel().select(row.getIndex()); - e.consume(); - } - }); - row.setOnDragOver(e -> { - if (e.getGestureSource()!=row && - e.getDragboard().hasContent(DataFormat.PLAIN_TEXT)) { - e.acceptTransferModes(TransferMode.MOVE); - } - e.consume(); - }); - row.setOnDragDropped(e -> { - moveAbsolute(row.getIndex()); - e.setDropCompleted(true); e.consume(); - }); - return row; - }); - } - - private List selectedUids() { - return table.getSelectionModel().getSelectedItems() - .stream().map(Row::uid).toList(); - } - private static String firstLetter(String s) { - return (s == null || s.isBlank()) ? "" : s.substring(0, 1).toUpperCase(); - } - private String fmtParams(QueueItem q, String itemType, String itemName) { - // Format args first - String a = Optional.ofNullable(q.args()).orElse(List.of()) - .stream().map(PythonParameterConverter::toPythonRepr) - .collect(Collectors.joining(", ")); - - // Get the plan/instruction definition to determine parameter order - Map itemInfo = null; - if ("plan".equals(itemType)) { - itemInfo = allowedPlans.get(itemName); - } else if ("instruction".equals(itemType)) { - itemInfo = allowedInstructions.get(itemName); - } - - // Format kwargs in schema order if available - String k; - if (itemInfo != null) { - List> parameters = (List>) itemInfo.get("parameters"); - if (parameters != null) { - Map kwargs = Optional.ofNullable(q.kwargs()).orElse(Map.of()); - // Format kwargs in the order they appear in the schema - k = parameters.stream() - .map(paramInfo -> (String) paramInfo.get("name")) - .filter(kwargs::containsKey) - .map(paramName -> paramName + ": " + PythonParameterConverter.toPythonRepr(kwargs.get(paramName))) - .collect(Collectors.joining(", ")); - } else { - // No parameters defined, use default formatting - k = formatKwargsDefault(q.kwargs()); - } - } else { - // Plan/instruction not found in schema, use default formatting - k = formatKwargsDefault(q.kwargs()); - } - - return Stream.of(a, k).filter(s -> !s.isEmpty()) - .collect(Collectors.joining(", ")); - } - - private String formatKwargsDefault(Map kwargs) { - return Optional.ofNullable(kwargs).orElse(Map.of()) - .entrySet().stream() - .map(e -> e.getKey() + ": " + PythonParameterConverter.toPythonRepr(e.getValue())) - .collect(Collectors.joining(", ")); - } - private record Row(String uid, String itemType, String name, - String params, String user, String group) {} - - - private void notifySelectionChange() { - var selectedItems = table.getSelectionModel().getSelectedItems(); - QueueItem selectedItem = null; - - if (selectedItems.size() == 1) { - Row selectedRow = selectedItems.get(0); - selectedItem = uid2item.get(selectedRow.uid()); - } - - // Track the last selected UID for insert-after operations - // For multi-select, use the last item in the batch - String lastUid = null; - if (!selectedItems.isEmpty()) { - Row lastRow = selectedItems.get(selectedItems.size() - 1); - lastUid = lastRow.uid(); - } - - QueueItemSelectionEvent event = QueueItemSelectionEvent.getInstance(); - event.setLastSelectedUid(lastUid); - event.notifySelectionChanged(selectedItem); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java deleted file mode 100644 index 68c535a66f..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java +++ /dev/null @@ -1,505 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.api.*; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.PlansCache; -import org.phoebus.applications.queueserver.util.QueueItemSelectionEvent; -import org.phoebus.applications.queueserver.util.PythonParameterConverter; -import org.phoebus.applications.queueserver.util.StatusBus; -import org.phoebus.applications.queueserver.view.PlanEditEvent; -import org.phoebus.applications.queueserver.view.TabSwitchEvent; -import org.phoebus.applications.queueserver.view.ItemUpdateEvent; -import javafx.application.Platform; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.*; -import javafx.scene.control.cell.CheckBoxTableCell; -import javafx.scene.control.cell.PropertyValueFactory; - -import java.net.URL; -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class RePlanViewerController implements Initializable { - - @FXML private TableColumn paramCol; - @FXML private TableColumn chkCol; - @FXML private TableColumn valueCol; - @FXML private CheckBox paramChk; - @FXML private Label planLabel; - @FXML private TableView table; - @FXML private Button copyBtn, editBtn; - - private final RunEngineService svc = new RunEngineService(); - private static final Logger logger = Logger.getLogger(RePlanViewerController.class.getPackageName()); - private final ObservableList parameterRows = FXCollections.observableArrayList(); - private final Map> allowedPlans = new HashMap<>(); - private final Map> allowedInstructions = new HashMap<>(); - - private PythonParameterConverter getPythonConverter() { - PythonParameterConverter converter = PythonParameterConverter.getShared(); - if (converter == null) { - // Shared init not done yet - create inline (rare fallback) - converter = new PythonParameterConverter(); - } - return converter; - } - - private QueueItem currentQueueItem; - private String queueItemName = "-"; - private String queueItemType = null; - private boolean detailedView = true; // Show all parameters by default - private String currentUser = "GUI Client"; - private String currentUserGroup = "root"; - - // Selection change handler - to be called from queue controller - private Runnable onEditItemRequested; - - public static class ParameterRow { - private final SimpleStringProperty name; - private final SimpleBooleanProperty enabled; - private final SimpleStringProperty value; - private final SimpleStringProperty description; - private final SimpleBooleanProperty isOptional; - private final Object defaultValue; - - public ParameterRow(String name, boolean enabled, String value, String description, - boolean isOptional, Object defaultValue) { - this.name = new SimpleStringProperty(name); - this.enabled = new SimpleBooleanProperty(enabled); - this.value = new SimpleStringProperty(value != null ? value : ""); - this.description = new SimpleStringProperty(description != null ? description : ""); - this.isOptional = new SimpleBooleanProperty(isOptional); - this.defaultValue = defaultValue; - } - - public SimpleStringProperty nameProperty() { return name; } - public SimpleBooleanProperty enabledProperty() { return enabled; } - public SimpleStringProperty valueProperty() { return value; } - public SimpleStringProperty descriptionProperty() { return description; } - public SimpleBooleanProperty isOptionalProperty() { return isOptional; } - - public String getName() { return name.get(); } - public boolean isEnabled() { return enabled.get(); } - public String getValue() { return value.get(); } - public String getDescription() { return description.get(); } - public boolean isOptional() { return isOptional.get(); } - public Object getDefaultValue() { return defaultValue; } - } - - @Override - public void initialize(URL location, ResourceBundle resources) { - initializeTable(); - initializeControls(); - initializeAllowedInstructions(); - - // Use shared plans cache - copy to local map when loaded - if (PlansCache.isLoaded()) { - allowedPlans.clear(); - allowedPlans.putAll(PlansCache.get()); - } - - // Listen for plans cache updates - PlansCache.addListener((o, oldV, newV) -> { - Platform.runLater(() -> { - allowedPlans.clear(); - if (newV != null) { - allowedPlans.putAll(newV); - } - }); - }); - - // Listen for status changes to update widget state and clear on disconnect - StatusBus.addListener((o, oldV, newV) -> { - // Clear the displayed item when disconnected - if (newV == null) { - Platform.runLater(() -> showItem(null)); - } - Platform.runLater(this::updateWidgetState); - }); - - QueueItemSelectionEvent.getInstance().addListener(this::showItem); - - ItemUpdateEvent.getInstance().addListener(this::handleItemUpdate); - } - - private void initializeAllowedInstructions() { - Map queueStopInstr = new HashMap<>(); - queueStopInstr.put("name", "queue_stop"); - queueStopInstr.put("description", "Stop execution of the queue."); - queueStopInstr.put("parameters", List.of()); - allowedInstructions.put("queue_stop", queueStopInstr); - } - - private void initializeTable() { - paramCol.setCellValueFactory(new PropertyValueFactory<>("name")); - chkCol.setCellValueFactory(new PropertyValueFactory<>("enabled")); - valueCol.setCellValueFactory(new PropertyValueFactory<>("value")); - - // Fix paramCol and chkCol widths - not resizable - paramCol.setMinWidth(120); - paramCol.setPrefWidth(120); - paramCol.setMaxWidth(120); - paramCol.setResizable(false); - - chkCol.setMinWidth(30); - chkCol.setPrefWidth(30); - chkCol.setMaxWidth(30); - chkCol.setResizable(false); - - // valueCol fills remaining space - valueCol.setMinWidth(50); - - paramCol.setCellFactory(column -> { - TableCell cell = new TableCell() { - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || getTableRow() == null || getTableRow().getItem() == null) { - setText(null); - setTooltip(null); - } else { - setText(item); - ParameterRow row = getTableRow().getItem(); - if (row != null && row.getDescription() != null && !row.getDescription().isEmpty()) { - Tooltip tooltip = new Tooltip(row.getDescription()); - tooltip.setWrapText(true); - tooltip.setMaxWidth(300); - setTooltip(tooltip); - } - } - } - }; - return cell; - }); - - chkCol.setCellFactory(column -> { - CheckBoxTableCell cell = new CheckBoxTableCell<>(); - cell.setEditable(false); - return cell; - }); - - valueCol.setCellFactory(column -> { - TableCell cell = new TableCell() { - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || getTableRow() == null || getTableRow().getItem() == null) { - setText(null); - setTooltip(null); - } else { - setText(item); - ParameterRow row = getTableRow().getItem(); - if (!row.isEnabled()) { - setStyle("-fx-text-fill: grey;"); - } else { - setStyle(""); - } - - // Add tooltip with parameter description - if (row.getDescription() != null && !row.getDescription().isEmpty()) { - Tooltip tooltip = new Tooltip(row.getDescription()); - tooltip.setWrapText(true); - tooltip.setMaxWidth(300); - setTooltip(tooltip); - } - } - } - }; - return cell; - }); - - table.setItems(parameterRows); - table.setEditable(false); // Viewer is read-only - - // Use constrained resize so valueCol fills remaining space - table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - } - - private void initializeControls() { - paramChk.setSelected(detailedView); - paramChk.selectedProperty().addListener((obs, oldVal, newVal) -> { - detailedView = newVal; - if (currentQueueItem != null) { - showItem(currentQueueItem); - } - }); - - copyBtn.setOnAction(e -> copyToQueue()); - - editBtn.setOnAction(e -> editCurrentItem()); - - updateWidgetState(); - } - - public void showItem(QueueItem item) { - // Skip if same state (avoid unnecessary refresh/flash) - if (item == null && currentQueueItem == null) return; - if (item != null && currentQueueItem != null && - item.itemUid() != null && item.itemUid().equals(currentQueueItem.itemUid())) { - return; - } - - currentQueueItem = item; - - String defaultName = "-"; - queueItemName = item != null ? (item.name() != null ? item.name() : defaultName) : defaultName; - queueItemType = item != null ? item.itemType() : null; - - String displayedItemType = "instruction".equals(queueItemType) ? "Instruction: " : "Plan: "; - planLabel.setText(displayedItemType + queueItemName); - - setItemDescriptionTooltip(); - - updateWidgetState(); - loadParametersForItem(item); - } - - private void setItemDescriptionTooltip() { - if (currentQueueItem != null) { - Map itemInfo; - if ("plan".equals(queueItemType)) { - itemInfo = allowedPlans.get(queueItemName); - } else { - itemInfo = allowedInstructions.get(queueItemName); - } - - if (itemInfo != null) { - String description = (String) itemInfo.get("description"); - if (description != null && !description.isEmpty()) { - Tooltip tooltip = new Tooltip(description); - tooltip.setWrapText(true); - tooltip.setMaxWidth(400); - planLabel.setTooltip(tooltip); - } else { - planLabel.setTooltip(new Tooltip("Description for '" + queueItemName + "' was not found...")); - } - } - } else { - planLabel.setTooltip(null); - } - } - - private void loadParametersForItem(QueueItem item) { - if (item == null) { - parameterRows.clear(); - return; - } - - Map itemInfo; - if ("plan".equals(item.itemType())) { - itemInfo = allowedPlans.get(item.name()); - } else { - itemInfo = allowedInstructions.get(item.name()); - } - - if (itemInfo == null) return; - - List> parameters = (List>) itemInfo.get("parameters"); - if (parameters == null) parameters = new ArrayList<>(); - - Map itemKwargs = item.kwargs() != null ? item.kwargs() : new HashMap<>(); - - // Process parameters in background thread to avoid blocking UI with Python parsing - final List> finalParameters = parameters; - final Map finalItemInfo = itemInfo; - new Thread(() -> { - List newRows = new ArrayList<>(); - - for (Map paramInfo : finalParameters) { - String paramName = (String) paramInfo.get("name"); - String description = (String) paramInfo.get("description"); - if (description == null || description.isEmpty()) { - description = "Description for parameter '" + paramName + "' was not found..."; - } - Object defaultValue = paramInfo.get("default"); - boolean isOptional = defaultValue != null || "VAR_POSITIONAL".equals(paramInfo.get("kind")) || - "VAR_KEYWORD".equals(paramInfo.get("kind")); - - String currentValue = ""; - boolean isEnabled = false; - - if (itemKwargs.containsKey(paramName)) { - Object value = itemKwargs.get(paramName); - currentValue = value != null ? PythonParameterConverter.toPythonRepr(value) : ""; - isEnabled = true; - } else if (defaultValue != null) { - // Use normalizeAndRepr for defaults - they might be strings that need parsing - currentValue = getPythonConverter().normalizeAndRepr(defaultValue); - isEnabled = false; - } - - boolean shouldShow = detailedView || isEnabled; - - if (shouldShow) { - ParameterRow row = new ParameterRow(paramName, isEnabled, currentValue, description, isOptional, defaultValue); - newRows.add(row); - } - } - - // Add metadata rows in background too - List metadataRows = buildMetadataRows(item); - - // Update UI on FX thread - Platform.runLater(() -> { - parameterRows.setAll(newRows); - parameterRows.addAll(metadataRows); - }); - }).start(); - } - - private List buildMetadataRows(QueueItem item) { - List rows = new ArrayList<>(); - if (item.result() == null) { - return rows; - } - - Map result = item.result(); - - if (!result.isEmpty()) { - ParameterRow separator = new ParameterRow("--- Metadata & Results ---", false, "", - "Execution metadata and results", false, null); - rows.add(separator); - } - - for (Map.Entry entry : result.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - String displayValue = formatResultValue(value); - String description = "Result field: " + key; - - ParameterRow row = new ParameterRow(key, true, displayValue, description, false, null); - rows.add(row); - } - return rows; - } - - private String formatResultValue(Object value) { - if (value == null) { - return "None"; - } - - if (value instanceof Map) { - Map map = (Map) value; - if (map.isEmpty()) { - return "{}"; - } - // For small maps, show the full Python repr - if (map.size() <= 3) { - return PythonParameterConverter.toPythonRepr(value); - } - return "Map (" + map.size() + " entries)"; - } - - if (value instanceof List) { - List list = (List) value; - if (list.isEmpty()) { - return "[]"; - } - // For small lists, show the full Python repr - if (list.size() <= 5) { - return PythonParameterConverter.toPythonRepr(value); - } - return "List (" + list.size() + " items)"; - } - - if (value instanceof String) { - String str = (String) value; - if (str.length() > 100) { - return "'" + str.substring(0, 97) + "...'"; - } - return PythonParameterConverter.toPythonRepr(value); - } - - return PythonParameterConverter.toPythonRepr(value); - } - - private void updateWidgetState() { - StatusResponse status = StatusBus.latest().get(); - boolean isConnected = status != null; - - boolean isItemAllowed = false; - if (queueItemType != null && queueItemName != null && !"-".equals(queueItemName)) { - if ("plan".equals(queueItemType)) { - isItemAllowed = allowedPlans.get(queueItemName) != null; - } else if ("instruction".equals(queueItemType)) { - isItemAllowed = allowedInstructions.get(queueItemName) != null; - } - } - - // Disable controls when not connected (environment open not required) - table.setDisable(!isConnected); - paramChk.setDisable(!isConnected); - copyBtn.setDisable(!isConnected || !isItemAllowed); - editBtn.setDisable(!isConnected || !isItemAllowed); - } - - private void copyToQueue() { - if (currentQueueItem == null) { - return; - } - - // Capture insert position before starting background thread - String afterUid = QueueItemSelectionEvent.getInstance().getLastSelectedUid(); - - QueueItem itemCopy = new QueueItem( - currentQueueItem.itemType(), - currentQueueItem.name(), - currentQueueItem.args() != null ? currentQueueItem.args() : List.of(), - currentQueueItem.kwargs() != null ? currentQueueItem.kwargs() : new HashMap<>(), - null, - currentUser, - currentUserGroup, - null - ); - - QueueItemAdd request = new QueueItemAdd( - new QueueItemAdd.Item(itemCopy.itemType(), itemCopy.name(), itemCopy.args(), itemCopy.kwargs()), - currentUser, - currentUserGroup, - afterUid - ); - - new Thread(() -> { - try { - String newUid = svc.addItemGetUid(request); - QueueItemSelectionEvent.getInstance().requestSelectByUids(List.of(newUid)); - } catch (Exception e) { - logger.log(Level.WARNING, "Copy to queue error", e); - } - }).start(); - } - - private void editCurrentItem() { - if (currentQueueItem != null) { - PlanEditEvent.getInstance().notifyEditRequested(currentQueueItem); - TabSwitchEvent.getInstance().switchToTab("Plan Editor"); - } - } - - public void setOnEditItemRequested(Runnable callback) { - this.onEditItemRequested = callback; - } - - public QueueItem getCurrentItem() { - return currentQueueItem; - } - - private void handleItemUpdate(QueueItem updatedItem) { - // If this is the item currently being viewed, refresh the display - if (currentQueueItem != null && - updatedItem != null && - updatedItem.itemUid() != null && - updatedItem.itemUid().equals(currentQueueItem.itemUid())) { - - // Update the current item and refresh the display - Platform.runLater(() -> showItem(updatedItem)); - } - } - -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java deleted file mode 100644 index 124238a063..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.application.Platform; -import javafx.beans.value.ObservableValue; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.control.Label; - -import java.net.URL; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class ReQueueControlsController implements Initializable { - - @FXML private CheckBox autoChk; - @FXML private Label reStatusLabel; - @FXML private Button startBtn; - @FXML private Button stopBtn; // text toggles “Stop” / “Cancel Stop” - - private final RunEngineService svc = new RunEngineService(); - private static final Logger logger = Logger.getLogger(ReQueueControlsController.class.getPackageName()); - - private volatile boolean autoEnabledSrv = false; // what server says now - private volatile boolean stopPendingSrv = false; // ditto - - @Override public void initialize(URL url, ResourceBundle rb) { - - StatusBus.addListener(this::onStatus); - // Check current value in case status was already set before listener added - refreshButtons(StatusBus.latest().get()); - - startBtn.setOnAction(e -> runSafely("queueStart", () -> { - try { - svc.queueStart(); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - })); - - stopBtn.setOnAction(e -> { - if (!stopPendingSrv) - runSafely("queueStop", () -> { - try { - svc.queueStop(); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - }); - else - runSafely("queueStopCancel", () -> { - try { - svc.queueStopCancel(); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - }); - }); - - autoChk.setOnAction(e -> { - boolean want = autoChk.isSelected(); - if (want != autoEnabledSrv) { - runSafely("queueAutostart", - () -> { - try { - svc.queueAutostart(Map.of("enable", want)); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - }); - } - }); - } - - private void onStatus(ObservableValue src, Object oldV, Object newV) { - Platform.runLater(() -> refreshButtons(newV)); - } - - @SuppressWarnings("unchecked") - private void refreshButtons(Object statusObj) { - - boolean connected = statusObj != null; - boolean workerExists = false; - boolean running = false; - String managerState = null; - boolean queueStopPending = false; - boolean queueAutoEnabled = false; - - if (statusObj instanceof StatusResponse s) { - workerExists = s.workerEnvironmentExists(); - running = s.runningItemUid() != null; - managerState = s.managerState(); - queueStopPending = s.queueStopPending(); - queueAutoEnabled = s.queueAutostartEnabled(); - } - else if (statusObj instanceof Map m) { - workerExists = Boolean.TRUE.equals(m.get("worker_environment_exists")); - running = m.get("running_item_uid") != null; - managerState = String.valueOf(m.get("manager_state")); - queueStopPending = Boolean.TRUE.equals(m.get("queue_stop_pending")); - queueAutoEnabled = Boolean.TRUE.equals(m.get("queue_autostart_enabled")); - } - - autoEnabledSrv = queueAutoEnabled; - stopPendingSrv = queueStopPending; - - boolean idle = "idle".equals(managerState); - - reStatusLabel.setText(running ? "RUNNING" : "STOPPED"); - - boolean startEnabled = - connected && workerExists && !running && !queueAutoEnabled; - startBtn.setDisable(!startEnabled); - - stopBtn.setText(queueStopPending ? "Cancel Stop" : "Stop"); - stopBtn.setDisable(!(connected && workerExists && running)); - - autoChk.setDisable(!(connected && workerExists)); - autoChk.setSelected(queueAutoEnabled); - } - - private void runSafely(String what, Runnable r) { - try { r.run(); } - catch (Exception ex) { logger.log(Level.WARNING, what + ": " + ex); } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java deleted file mode 100644 index fd233ba405..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java +++ /dev/null @@ -1,200 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.api.QueueGetPayload; -import org.phoebus.applications.queueserver.api.QueueItem; -import org.phoebus.applications.queueserver.api.QueueItemAdd; -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.client.RunEngineService; -import org.phoebus.applications.queueserver.util.QueueItemSelectionEvent; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.beans.value.ChangeListener; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Button; -import javafx.scene.control.TextArea; - -import java.net.URL; -import java.util.List; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class ReRunningPlanController implements Initializable { - - @FXML private Button copyBtn; - @FXML private Button updateBtn; - @FXML private TextArea planTextArea; - - private final RunEngineService svc = new RunEngineService(); - private static final Logger logger = Logger.getLogger(ReRunningPlanController.class.getPackageName()); - - private String lastRunningUid = ""; - private QueueItem cachedRunningItem = null; - - private final boolean viewOnly; - - public ReRunningPlanController() { - this(false); // default to editable - } - - public ReRunningPlanController(boolean viewOnly) { - this.viewOnly = viewOnly; - } - - @Override public void initialize(URL u, ResourceBundle b) { - planTextArea.setEditable(false); - planTextArea.setStyle("-fx-focus-color: transparent;"); - planTextArea.setStyle("-fx-faint-focus-color: transparent;"); - - if (viewOnly) { - copyBtn.setDisable(true); - updateBtn.setDisable(true); - } - - render(StatusBus.latest().get()); - ChangeListener statusL = (obs, o, n) -> render(n); - StatusBus.addListener(statusL); - } - - private void render(StatusResponse st) { - if (st == null) { - // Don't clear running plan - keep last data visible for users - // Disable buttons when there's no status (disconnected or status error) - if (!viewOnly) { - copyBtn.setDisable(true); - updateBtn.setDisable(true); - } - return; - } - - boolean envExists = st.workerEnvironmentExists(); - String mgrState = nz(st.managerState()); - String wkrState = nz(st.workerEnvironmentState()); - String ipkState = nz(st.ipKernelState()); - boolean runningNow = st.runningItemUid() != null; - - copyBtn .setDisable(!runningNow); // RE monitor-mode does not exist in FX yet - - boolean canUpd = - envExists && - "idle".equals(mgrState) && - "idle".equals(wkrState) && - !"busy".equals(ipkState); - updateBtn.setDisable(!canUpd); - - String uid = st.runningItemUid(); - if (uid == null) { // nothing running - planTextArea.clear(); - lastRunningUid = ""; - cachedRunningItem = null; - return; - } - - // Fetch running item only if it's a new plan - if (!uid.equals(lastRunningUid)) { - cachedRunningItem = fetchRunningItem(); - lastRunningUid = uid; - } - - // Always fetch the latest run list - List> runList = fetchRunList(); - - // Use cached running item to keep displaying plan details - planTextArea.setText(format(cachedRunningItem, runList)); - planTextArea.positionCaret(0); - } - - private QueueItem fetchRunningItem() { - try { - QueueGetPayload p = svc.queueGetTyped(); - return p.runningItem(); // may be null - } catch (Exception ex) { - logger.log(Level.FINE, "Failed to fetch running item: " + ex.getMessage()); - return null; - } - } - - @SuppressWarnings("unchecked") - private List> fetchRunList() { - try { - var env = svc.reRuns(null); - Object p = env.payload(); - if (p instanceof Map m && m.containsKey("run_list")) - return (List>) m.get("run_list"); - } catch (Exception ex) { - logger.log(Level.FINE, "Failed to fetch run list: " + ex.getMessage()); - } - return List.of(); - } - - private static String format(QueueItem item, List> runs) { - StringBuilder sb = new StringBuilder(); - - if (item != null) { - sb.append("Plan Name: ").append(item.name()).append('\n'); - - if (!item.args().isEmpty()) - sb.append("Arguments: ") - .append(String.join(", ", item.args().stream() - .map(Object::toString).toList())) - .append('\n'); - - if (!item.kwargs().isEmpty()) { - sb.append("Parameters:\n"); - item.kwargs().forEach((k,v) -> - sb.append(" ").append(k).append(": ").append(v).append('\n')); - } - - sb.append('\n'); - } - - if (!runs.isEmpty()) { - sb.append("Runs:\n"); - for (Map r : runs) { - String uid = String.valueOf(r.get("uid")); - boolean open= Boolean.TRUE.equals(r.get("is_open")); - sb.append(" ").append(uid).append(" "); - sb.append(open ? "In progress ..." : - "Exit status: " + r.get("exit_status")); - sb.append('\n'); - } - } - return sb.toString(); - } - - @FXML - private void copyToQueue() { - if (cachedRunningItem == null) return; - - // Capture insert position before starting background thread - String afterUid = QueueItemSelectionEvent.getInstance().getLastSelectedUid(); - - QueueItem item = cachedRunningItem; - new Thread(() -> { - try { - QueueItemAdd req = new QueueItemAdd( - QueueItemAdd.Item.from(item), - "GUI Client", "primary", afterUid); - String newUid = svc.addItemGetUid(req); - QueueItemSelectionEvent.getInstance().requestSelectByUids(List.of(newUid)); - logger.log(Level.FINE, "Copied running plan to queue: " + item.name()); - } catch (Exception ex) { - logger.log(Level.WARNING, "Failed to copy running plan to queue", ex); - } - }).start(); - } - - - @FXML - private void updateEnvironment() { - try { - svc.environmentUpdate(Map.of()); - logger.log(Level.FINE, "Environment update requested"); - } catch (Exception ex) { - logger.log(Level.WARNING, "Failed to update environment", ex); - } - } - - private static String nz(String s) { return s == null ? "" : s; } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReStatusMonitorController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReStatusMonitorController.java deleted file mode 100644 index 8a7b6ea0f1..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReStatusMonitorController.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.phoebus.applications.queueserver.controller; - -import org.phoebus.applications.queueserver.api.StatusResponse; -import org.phoebus.applications.queueserver.util.StatusBus; -import javafx.beans.value.ChangeListener; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Label; - -import java.net.URL; -import java.util.ResourceBundle; - -public final class ReStatusMonitorController implements Initializable { - - @FXML private Label envLabel; - @FXML private Label managerLabel; - @FXML private Label reLabel; - @FXML private Label histLabel; - @FXML private Label autoLabel; - @FXML private Label stopLabel; - @FXML private Label loopLabel; - @FXML private Label queueLabel; - - private ChangeListener listener; - - @Override public void initialize(URL url, ResourceBundle rb) { - - render(StatusBus.latest().get()); - - listener = (obs, oldVal, newVal) -> render(newVal); - StatusBus.addListener(listener); - } - - public void shutdown() { - StatusBus.removeListener(listener); - } - - private void render(StatusResponse s) { - if (s == null) { // offline / not connected - dashAll(); - return; - } - - set(envLabel, "RE Environment: ", s.workerEnvironmentExists() ? "OPEN" : "CLOSED"); - set(managerLabel,"Manager state: ", uc(s.managerState())); - set(reLabel, "RE state: ", uc(s.reState())); - set(histLabel, "Items in history: ",s.itemsInHistory()); - set(queueLabel, "Items in queue: ", s.itemsInQueue()); - set(autoLabel, "Queue AUTOSTART: ", s.queueAutostartEnabled() ? "ON" : "OFF"); - set(stopLabel, "Queue STOP pending: ", s.queueStopPending() ? "YES" : "NO"); - - Boolean loop = s.planQueueMode() == null ? null : s.planQueueMode().loop(); - set(loopLabel, "Queue LOOP mode: ", loop == null ? "—" : (loop ? "ON" : "OFF")); - } - - private void dashAll() { - set(envLabel, "RE Environment: ", "-"); - set(managerLabel,"Manager state: ", "-"); - set(reLabel, "RE state: ", "-"); - set(histLabel, "Items in history: ", "-"); - set(queueLabel, "Items in queue: ", "-"); - set(autoLabel, "Queue AUTOSTART: ", "-"); - set(stopLabel, "Queue STOP pending: ","-"); - set(loopLabel, "Queue LOOP mode: ", "-"); - } - - private static void set(Label lbl, String prefix, Object value) { - lbl.setText(prefix + (value == null ? "-" : String.valueOf(value))); - } - private static String uc(String s) { return s == null ? null : s.toUpperCase(); } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/AppLifecycle.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/AppLifecycle.java deleted file mode 100644 index 0c6e27e61c..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/AppLifecycle.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import org.phoebus.applications.queueserver.view.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Manages application lifecycle - especially cleanup when the app is closed - * so that reopening within Phoebus gets a fresh state. - */ -public final class AppLifecycle { - - private static final Logger logger = Logger.getLogger(AppLifecycle.class.getPackageName()); - - private static final List shutdownCallbacks = new ArrayList<>(); - - private AppLifecycle() {} - - /** - * Register a shutdown callback. - * Called by controllers during initialization. - */ - public static void registerShutdown(Runnable shutdown) { - shutdownCallbacks.add(shutdown); - } - - /** - * Reset all static state for app restart. - * Called by QueueServerInstance when the app tab is closed. - */ - public static void shutdown() { - try { - logger.log(Level.INFO, "Queue Server app shutting down - resetting state"); - - // Run all registered shutdown callbacks - for (Runnable callback : shutdownCallbacks) { - try { - callback.run(); - } catch (Exception e) { - logger.log(Level.WARNING, "Error during shutdown callback", e); - } - } - shutdownCallbacks.clear(); - - // Reset all event buses - these just clear listeners, no callbacks fired - try { StatusBus.reset(); } catch (Exception e) { /* ignore */ } - try { QueueItemSelectionEvent.getInstance().reset(); } catch (Exception e) { /* ignore */ } - try { ItemUpdateEvent.getInstance().reset(); } catch (Exception e) { /* ignore */ } - try { PlanEditEvent.getInstance().reset(); } catch (Exception e) { /* ignore */ } - try { TabSwitchEvent.getInstance().reset(); } catch (Exception e) { /* ignore */ } - try { UiSignalEvent.reset(); } catch (Exception e) { /* ignore */ } - try { PlansCache.reset(); } catch (Exception e) { /* ignore */ } - - // Reset Python converter so it can be re-initialized on next app open - try { PythonParameterConverter.resetShared(); } catch (Exception e) { /* ignore */ } - - logger.log(Level.INFO, "Queue Server app state reset complete"); - } catch (Exception e) { - logger.log(Level.WARNING, "Error during app shutdown", e); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java deleted file mode 100644 index 051206483d..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java +++ /dev/null @@ -1,34 +0,0 @@ -// src/main/java/com/jbi/util/HttpSupport.java -package org.phoebus.applications.queueserver.util; - -import java.net.http.HttpRequest; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -public final class HttpSupport { - - private HttpSupport() {} - - public static final Logger logger = Logger.getLogger(HttpSupport.class.getPackageName()); - - /* ---------------- retry policy ----------- */ - public static final int MAX_RETRIES = 3; - public static final long INITIAL_BACKOFF_MS = 200; - public static final double BACKOFF_MULTIPLIER = 2.0; - - // Only these methods are idempotent → safe to retry - private static final Set IDEMPOTENT = Set.of("GET", "DELETE"); - - public static boolean isRetryable(HttpRequest req) { - return IDEMPOTENT.contains(req.method()); - } - - public static long elapsed(long startNano) { - return (System.nanoTime() - startNano) / 1_000_000; - } - - public static void fine(String msg) { - logger.log(Level.FINE, msg); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/JythonScriptExecutor.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/JythonScriptExecutor.java deleted file mode 100644 index d6fc6b4e6f..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/JythonScriptExecutor.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import org.python.core.*; -import org.python.util.PythonInterpreter; - -import java.io.InputStream; -import java.util.concurrent.*; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Utility class for executing Jython scripts in the Queue Server application. - * Based on Phoebus display runtime JythonScriptSupport implementation. - */ -public class JythonScriptExecutor { - - private static final Logger logger = Logger.getLogger(JythonScriptExecutor.class.getPackageName()); - private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(r -> { - Thread thread = new Thread(r, "QueueServer-Jython"); - thread.setDaemon(true); - return thread; - }); - - private final PythonInterpreter python; - - static { - // Configure Jython options (similar to Phoebus display runtime) - PySystemState.initialize(); - Options.dont_write_bytecode = true; - - // Set console encoding - PySystemState sys = Py.getSystemState(); - sys.setdefaultencoding("utf-8"); - - logger.log(Level.FINE, "Jython initialized for Queue Server"); - } - - /** - * Create a new Jython script executor with a dedicated interpreter instance. - */ - public JythonScriptExecutor() { - // Synchronized to prevent concurrent initialization issues - synchronized (JythonScriptExecutor.class) { - this.python = new PythonInterpreter(); - } - } - - /** - * Execute a Python script with the given context variables. - * - * @param scriptContent The Python script code to execute - * @param contextVars Variables to pass into the Python context (key -> value pairs) - * @return The result object returned by the script (or null if none) - */ - public Object execute(String scriptContent, java.util.Map contextVars) { - try { - // Set context variables in the Python interpreter - if (contextVars != null) { - for (java.util.Map.Entry entry : contextVars.entrySet()) { - python.set(entry.getKey(), entry.getValue()); - } - } - - // Execute the script - python.exec(scriptContent); - - // Try to get a result variable if one was set - PyObject result = python.get("result"); - - // Clear context to prevent memory leaks - if (contextVars != null) { - for (String key : contextVars.keySet()) { - python.set(key, null); - } - } - - // Convert PyObject to Java object - return result != null ? result.__tojava__(Object.class) : null; - - } catch (Exception e) { - logger.log(Level.WARNING, "Jython script execution failed", e); - throw new RuntimeException("Script execution failed: " + e.getMessage(), e); - } - } - - /** - * Execute a Python script asynchronously. - * - * @param scriptContent The Python script code to execute - * @param contextVars Variables to pass into the Python context - * @return Future that will contain the result - */ - public Future executeAsync(String scriptContent, java.util.Map contextVars) { - return EXECUTOR.submit(() -> execute(scriptContent, contextVars)); - } - - /** - * Execute a Python script from a resource file. - * - * @param resourcePath Path to the Python script resource - * @param contextVars Variables to pass into the Python context - * @return The result object returned by the script - */ - public Object executeResource(String resourcePath, java.util.Map contextVars) throws Exception { - try (InputStream stream = getClass().getResourceAsStream(resourcePath)) { - if (stream == null) { - throw new IllegalArgumentException("Resource not found: " + resourcePath); - } - - String scriptContent = new String(stream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); - return execute(scriptContent, contextVars); - } - } - - /** - * Execute a simple Python expression and return the result. - * - * @param expression Python expression to evaluate - * @return The evaluated result - */ - public Object eval(String expression) { - try { - PyObject result = python.eval(expression); - return result.__tojava__(Object.class); - } catch (Exception e) { - logger.log(Level.WARNING, "Jython eval failed for: " + expression, e); - throw new RuntimeException("Evaluation failed: " + e.getMessage(), e); - } - } - - /** - * Close this executor and release resources. - */ - public void close() { - if (python != null) { - python.close(); - } - } - - /** - * Shutdown the shared executor service (call during application shutdown). - */ - public static void shutdown() { - EXECUTOR.shutdown(); - try { - if (!EXECUTOR.awaitTermination(5, TimeUnit.SECONDS)) { - EXECUTOR.shutdownNow(); - } - } catch (InterruptedException e) { - EXECUTOR.shutdownNow(); - Thread.currentThread().interrupt(); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PlansCache.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PlansCache.java deleted file mode 100644 index ead627c280..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PlansCache.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import org.phoebus.applications.queueserver.client.RunEngineService; -import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ChangeListener; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Shared cache for allowed plans data. Fetches once and notifies all listeners. - * This avoids multiple controllers making redundant HTTP calls for the same data. - */ -public final class PlansCache { - - private static final Logger logger = Logger.getLogger(PlansCache.class.getPackageName()); - - private static final ObjectProperty>> PLANS = new SimpleObjectProperty<>(new HashMap<>()); - private static final List>>> listeners = new ArrayList<>(); - private static final AtomicBoolean loading = new AtomicBoolean(false); - private static final AtomicBoolean loaded = new AtomicBoolean(false); - - private PlansCache() {} - - /** - * Get the plans property for binding or adding listeners. - */ - public static ObjectProperty>> plans() { - return PLANS; - } - - /** - * Get the current plans map (never null, may be empty). - */ - public static Map> get() { - Map> p = PLANS.get(); - return p != null ? p : new HashMap<>(); - } - - /** - * Check if plans have been loaded. - */ - public static boolean isLoaded() { - return loaded.get(); - } - - /** - * Add a listener and track it for cleanup during reset. - */ - public static void addListener(ChangeListener>> listener) { - listeners.add(listener); - PLANS.addListener(listener); - } - - /** - * Remove a tracked listener. - */ - public static void removeListener(ChangeListener>> listener) { - listeners.remove(listener); - PLANS.removeListener(listener); - } - - /** - * Load plans from the server. Only makes one HTTP call even if called multiple times. - * After loading, all registered listeners are notified. - */ - public static void loadIfNeeded() { - // Already loaded or currently loading - skip - if (loaded.get() || !loading.compareAndSet(false, true)) { - return; - } - - logger.log(Level.FINE, "Loading allowed plans (shared cache)"); - - new Thread(() -> { - try { - RunEngineService svc = new RunEngineService(); - Map responseMap = svc.plansAllowedRaw(); - - Map> newPlans = new HashMap<>(); - if (responseMap != null && Boolean.TRUE.equals(responseMap.get("success"))) { - if (responseMap.containsKey("plans_allowed")) { - @SuppressWarnings("unchecked") - Map> plans = - (Map>) responseMap.get("plans_allowed"); - if (plans != null) { - newPlans.putAll(plans); - } - } - } - - final Map> result = newPlans; - Platform.runLater(() -> { - PLANS.set(result); - loaded.set(true); - loading.set(false); - logger.log(Level.FINE, "Loaded " + result.size() + " allowed plans into shared cache"); - }); - - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to load allowed plans", e); - loading.set(false); - } - }, "PlansCache-Loader").start(); - } - - /** - * Force reload of plans (e.g., after reconnection). - */ - public static void reload() { - loaded.set(false); - loading.set(false); - loadIfNeeded(); - } - - /** - * Reset state for app restart. - */ - public static void reset() { - Runnable doReset = () -> { - for (ChangeListener>> listener : listeners) { - PLANS.removeListener(listener); - } - listeners.clear(); - PLANS.set(new HashMap<>()); - loaded.set(false); - loading.set(false); - }; - - if (Platform.isFxApplicationThread()) { - doReset.run(); - } else { - Platform.runLater(doReset); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java deleted file mode 100644 index 9ba3d4a106..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import java.util.concurrent.*; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; - -// Single ScheduledExecutor shared by all widgets that need periodic polling. -public final class PollCenter { - - private static final Logger logger = Logger.getLogger(PollCenter.class.getPackageName()); - private static final ScheduledExecutorService EXEC = - Executors.newSingleThreadScheduledExecutor(r -> - new Thread(r, "JBI-poller")); - - private PollCenter() {} - - public static ScheduledFuture every(long periodSec, Runnable task) { - logger.log(Level.FINE, "Scheduling task with period: " + periodSec + " seconds"); - return EXEC.scheduleAtFixedRate(task, 0, periodSec, TimeUnit.SECONDS); - } - - public static ScheduledFuture everyMs(long periodMs, Runnable task) { - logger.log(Level.FINE, "Scheduling task with period: " + periodMs + " milliseconds"); - return EXEC.scheduleAtFixedRate(task, 0, periodMs, TimeUnit.MILLISECONDS); - } - - public static ScheduledFuture afterMs(long delayMs, Runnable task) { - logger.log(Level.FINE, "Scheduling one-time task after: " + delayMs + " milliseconds"); - return EXEC.schedule(task, delayMs, TimeUnit.MILLISECONDS); - } - - public static ScheduledFuture every( - long periodSec, - Supplier supplier, - java.util.function.Consumer fxConsumer) { - - return every(periodSec, () -> { - try { - T t = supplier.get(); - javafx.application.Platform.runLater(() -> fxConsumer.accept(t)); - } catch (Exception ex) { - logger.log(Level.WARNING, "Polling task failed", ex); - } - }); - } - - public static ScheduledFuture everyMs( - long periodMs, - Supplier supplier, - java.util.function.Consumer fxConsumer) { - - return everyMs(periodMs, () -> { - try { - T t = supplier.get(); - javafx.application.Platform.runLater(() -> fxConsumer.accept(t)); - } catch (Exception ex) { - logger.log(Level.WARNING, "Polling task failed", ex); - } - }); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PythonParameterConverter.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PythonParameterConverter.java deleted file mode 100644 index 8fb1aee336..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PythonParameterConverter.java +++ /dev/null @@ -1,322 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Python-based parameter type converter for Queue Server. - * - * This class delegates type conversion to a Python script (using Jython), - * allowing us to use Python's ast.literal_eval for parsing parameter values - * instead of implementing complex type conversions in Java. - * - * Use {@link #getShared()} to get a shared instance that's initialized once - * in the background to avoid blocking the UI thread. - */ -public class PythonParameterConverter { - - private static final Logger logger = Logger.getLogger(PythonParameterConverter.class.getPackageName()); - private static final String SCRIPT_RESOURCE = "/org/phoebus/applications/queueserver/scripts/type_converter.py"; - - // Shared singleton instance - initialized once in background - private static volatile PythonParameterConverter sharedInstance; - private static volatile boolean initializationStarted = false; - private static final Object initLock = new Object(); - - private final ObjectMapper objectMapper = new ObjectMapper(); - private final JythonScriptExecutor executor; - private final String scriptContent; - - /** - * Get the shared singleton instance. This is the preferred way to get a converter. - * The instance is initialized lazily in the background on first call. - * May return null if initialization hasn't completed yet - caller should handle this. - */ - public static PythonParameterConverter getShared() { - if (sharedInstance == null && !initializationStarted) { - initializeInBackground(); - } - return sharedInstance; - } - - /** - * Start background initialization of the shared instance. - * Call this early at app startup to have the converter ready when needed. - */ - public static void initializeInBackground() { - synchronized (initLock) { - if (initializationStarted) { - return; // Already started - } - initializationStarted = true; - } - - new Thread(() -> { - try { - PythonParameterConverter converter = new PythonParameterConverter(); - // Warm up by doing a simple conversion - converter.normalizeAndRepr("warmup"); - sharedInstance = converter; - logger.log(Level.FINE, "Shared Python converter initialized and warmed up"); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to initialize shared Python converter", e); - initializationStarted = false; // Allow retry - } - }, "PythonConverter-SharedInit").start(); - } - - /** - * Reset the shared instance - call when app is closed to allow re-initialization. - */ - public static void resetShared() { - synchronized (initLock) { - if (sharedInstance != null) { - sharedInstance.close(); - sharedInstance = null; - } - initializationStarted = false; - } - } - - /** - * Create a new Python parameter converter. - * Prefer using {@link #getShared()} instead to avoid multiple initializations. - */ - public PythonParameterConverter() { - this.executor = new JythonScriptExecutor(); - - // Load the Python script from resources - try (var stream = getClass().getResourceAsStream(SCRIPT_RESOURCE)) { - if (stream == null) { - throw new IllegalStateException("Python converter script not found: " + SCRIPT_RESOURCE); - } - this.scriptContent = new String(stream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); - logger.log(Level.FINE, "Python type converter script loaded successfully"); - } catch (Exception e) { - logger.log(Level.SEVERE, "Failed to load Python converter script", e); - throw new RuntimeException("Failed to initialize Python converter", e); - } - } - - /** - * Parameter information for conversion. - */ - public static class ParameterInfo { - private String name; - private String value; - private boolean enabled; - private boolean isOptional; - private Object defaultValue; - - public ParameterInfo(String name, String value, boolean enabled, boolean isOptional, Object defaultValue) { - this.name = name; - this.value = value; - this.enabled = enabled; - this.isOptional = isOptional; - this.defaultValue = defaultValue; - } - - // Getters and setters - public String getName() { return name; } - public void setName(String name) { this.name = name; } - - public String getValue() { return value; } - public void setValue(String value) { this.value = value; } - - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - - public boolean isOptional() { return isOptional; } - public void setOptional(boolean optional) { isOptional = optional; } - - public Object getDefaultValue() { return defaultValue; } - public void setDefaultValue(Object defaultValue) { this.defaultValue = defaultValue; } - } - - /** - * Convert a list of parameters from string values to typed objects using Python. - * - * @param parameters List of parameter information - * @return Map of parameter names to their typed values - */ - public Map convertParameters(List parameters) { - try { - // Serialize parameters to JSON - String parametersJson = objectMapper.writeValueAsString(parameters); - - // Prepare context for Python script - Map context = new HashMap<>(); - context.put("parameters_json", parametersJson); - - // Execute the Python script - Object result = executor.execute(scriptContent, context); - - // Parse result JSON back to Map - if (result != null) { - String resultJson = result.toString(); - - // Check if result contains an error - Map resultMap = objectMapper.readValue(resultJson, - new TypeReference>() {}); - - if (resultMap.containsKey("error")) { - String errorMsg = "Python type conversion failed: " + resultMap.get("error"); - logger.log(Level.SEVERE, errorMsg); - throw new RuntimeException(errorMsg); - } - - return resultMap; - } - - return new HashMap<>(); - - } catch (Exception e) { - logger.log(Level.FINE, "Python parameter conversion failed: " + e.getMessage()); - throw new RuntimeException("Parameter conversion failed: " + e.getMessage(), e); - } - } - - - /** - * Validate a parameter value using Python. - * - * @param value String value to validate - * @return true if the value can be parsed, false otherwise - */ - public boolean validateValue(String value) { - if (value == null || value.trim().isEmpty()) { - return true; - } - - try { - List testParam = List.of( - new ParameterInfo("test", value, true, true, null) - ); - convertParameters(testParam); - return true; - } catch (Exception e) { - return false; - } - } - - /** - * Normalize a value by parsing it if it's a string, then converting to Python repr. - * This ensures that string representations of numbers like "1" become 1 (without quotes), - * while actual strings like "hello" become 'hello' (with quotes). - * - * @param value The value to normalize - * @return Python repr string with correct types - */ - public String normalizeAndRepr(Object value) { - if (value == null) { - return "None"; - } - - // If it's already a proper Java type (not String), just repr it - if (!(value instanceof String)) { - return toPythonRepr(value); - } - - // It's a string - try to parse it to see what type it really is - String strValue = (String) value; - if (strValue.trim().isEmpty()) { - return ""; - } - - try { - // Try to parse with Python to determine true type - List testParam = List.of( - new ParameterInfo("test", strValue, true, true, null) - ); - Map parsed = convertParameters(testParam); - - if (parsed.containsKey("test")) { - // Successfully parsed - use the parsed value's repr - return toPythonRepr(parsed.get("test")); - } - } catch (Exception e) { - // If parsing fails, treat as a string literal - } - - // Fallback - just repr the string as-is - return toPythonRepr(value); - } - - /** - * Convert a Java object to its Python string representation (repr). - * This preserves Python syntax including quotes around strings in lists/dicts. - * - * @param value The Java object to convert - * @return Python repr string - */ - public static String toPythonRepr(Object value) { - if (value == null) { - return "None"; - } - - // Handle strings - add single quotes - if (value instanceof String) { - String str = (String) value; - // Escape single quotes and backslashes - str = str.replace("\\", "\\\\").replace("'", "\\'"); - return "'" + str + "'"; - } - - // Handle booleans - Python uses True/False - if (value instanceof Boolean) { - return ((Boolean) value) ? "True" : "False"; - } - - // Handle numbers - convert to string directly - if (value instanceof Number) { - return value.toString(); - } - - // Handle lists - if (value instanceof List) { - List list = (List) value; - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < list.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(toPythonRepr(list.get(i))); - } - sb.append("]"); - return sb.toString(); - } - - // Handle maps/dicts - if (value instanceof Map) { - Map map = (Map) value; - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : map.entrySet()) { - if (!first) sb.append(", "); - first = false; - sb.append(toPythonRepr(entry.getKey())); - sb.append(": "); - sb.append(toPythonRepr(entry.getValue())); - } - sb.append("}"); - return sb.toString(); - } - - // Fallback - just use toString - return value.toString(); - } - - /** - * Close the converter and release resources. - */ - public void close() { - if (executor != null) { - executor.close(); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/QueueItemSelectionEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/QueueItemSelectionEvent.java deleted file mode 100644 index d60b73987f..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/QueueItemSelectionEvent.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import org.phoebus.applications.queueserver.api.QueueItem; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class QueueItemSelectionEvent { - - private static final QueueItemSelectionEvent INSTANCE = new QueueItemSelectionEvent(); - - private final List> listeners = new CopyOnWriteArrayList<>(); - private static final Logger logger = Logger.getLogger(QueueItemSelectionEvent.class.getPackageName()); - - /** The last selected item's UID (for inserting after) */ - private volatile String lastSelectedUid = null; - - /** - * Exact UIDs to select on the next queue refresh. - * Set by controllers after a successful add/copy HTTP call returns the new item UIDs. - * The queue controller checks this on every refresh and selects matching rows. - */ - private volatile List pendingSelectUids = null; - - private QueueItemSelectionEvent() {} - - public static QueueItemSelectionEvent getInstance() { - return INSTANCE; - } - - public void addListener(Consumer listener) { - listeners.add(listener); - } - - public void removeListener(Consumer listener) { - listeners.remove(listener); - } - - public void notifySelectionChanged(QueueItem selectedItem) { - for (Consumer listener : listeners) { - try { - listener.accept(selectedItem); - } catch (Exception e) { - logger.log(Level.WARNING, "Error in queue selection listener", e); - } - } - } - - /** - * Set the UID of the last selected item (for insert-after operations). - */ - public void setLastSelectedUid(String uid) { - this.lastSelectedUid = uid; - } - - /** - * Get the UID of the last selected item. - * Returns null if no item is selected, meaning items should be added at the back. - */ - public String getLastSelectedUid() { - return lastSelectedUid; - } - - /** - * Request that the queue controller select specific UIDs on the next refresh. - * Called from background threads after a successful add/copy HTTP response. - */ - public void requestSelectByUids(List uids) { - pendingSelectUids = List.copyOf(uids); - } - - /** - * Get the UIDs to select, or null if there are none pending. - */ - public List getPendingSelectUids() { - return pendingSelectUids; - } - - /** - * Clear pending selection (called after UIDs were found and selected, - * or when the operation failed). - */ - public void clearPendingSelect() { - pendingSelectUids = null; - } - - /** - * Check if there are UIDs waiting to be selected. - */ - public boolean hasPendingSelect() { - return pendingSelectUids != null; - } - - /** Reset state for app restart */ - public void reset() { - listeners.clear(); - lastSelectedUid = null; - pendingSelectUids = null; - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RateLimiter.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RateLimiter.java deleted file mode 100644 index bdca4205cc..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RateLimiter.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import java.util.concurrent.TimeUnit; - -/** - * Thread-safe token-bucket limiter.
- * Default - 10 permits / second. - */ -public final class RateLimiter { - - private final long capacity; // max tokens in the bucket - private final double refillPerNanos; // tokens added per nanosecond - private double tokens; - private long lastRefill; // nanoTime() - - public RateLimiter(double permitsPerSecond) { - if (permitsPerSecond <= 0) throw new IllegalArgumentException("permitsPerSecond must be > 0"); - this.capacity = Math.max(1, Math.round(permitsPerSecond)); - this.refillPerNanos = permitsPerSecond / TimeUnit.SECONDS.toNanos(1); - this.tokens = capacity; - this.lastRefill = System.nanoTime(); - } - - /** Blocks until a token is available, then consumes one. */ - public void acquire() throws InterruptedException { - synchronized (this) { - refill(); - while (tokens < 1.0) { - long sleepNanos = (long) Math.ceil((1.0 - tokens) / refillPerNanos); - TimeUnit.NANOSECONDS.timedWait(this, sleepNanos); - refill(); // wake-up → try again - } - tokens -= 1.0; - } - } - - private void refill() { - long now = System.nanoTime(); - double add = (now - lastRefill) * refillPerNanos; - if (add > 0) { - tokens = Math.min(capacity, tokens + add); - lastRefill = now; - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineCli.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineCli.java deleted file mode 100644 index 1a0018f9de..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineCli.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.applications.queueserver.Preferences; -import org.phoebus.applications.queueserver.client.*; -import org.phoebus.applications.queueserver.client.ApiEndpoint; -import org.phoebus.applications.queueserver.client.RunEngineHttpClient; -import org.phoebus.applications.queueserver.client.RunEngineHttpClient.*; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Map; - -public final class RunEngineCli { - - private static final ObjectMapper JSON = new ObjectMapper(); - - private static void usage() { - System.out.println(""" - Bluesky CLI - Usage: - java … BlueskyCli list - java … BlueskyCli [body] - - list Print all ApiEndpoint constants - ENDPOINT Any constant from ApiEndpoint (case-insensitive) - body JSON string - @file.json – read JSON from file - @- – read JSON from STDIN - """); - } - - public static void main(String[] args) throws Exception { - if (args.length == 0 || "help".equalsIgnoreCase(args[0])) { - usage(); return; - } - - RunEngineHttpClient.initialize(Preferences.queue_server_url, Preferences.api_key); - var client = RunEngineHttpClient.get(); - - try { - if ("list".equalsIgnoreCase(args[0])) { - Arrays.stream(ApiEndpoint.values()) - .map(Enum::name) - .sorted() - .forEach(System.out::println); - return; - } - - ApiEndpoint ep; - try { - ep = ApiEndpoint.valueOf(args[0].toUpperCase()); - } catch (IllegalArgumentException iae) { - System.err.println("❌ Unknown endpoint. Run `list` for valid names."); - System.exit(2); - return; - } - - Object body = (args.length > 1) ? parseBodyArg(args[1]) : null; - Map resp = client.send(ep, body); - System.out.println(JSON.writerWithDefaultPrettyPrinter().writeValueAsString(resp)); - } - catch (BlueskyException be) { - System.err.println("❌ " + be.getMessage()); - System.exit(2); - } - catch (Exception ex) { - System.err.println("❌ Unexpected error: " + ex); - System.exit(1); - } - } - - - private static Object parseBodyArg(String arg) throws IOException { - if (arg.startsWith("@")) { - if (arg.equals("@-")) { - return JSON.readValue(System.in, Object.class); - } else { - Path p = Path.of(arg.substring(1)); - return JSON.readValue(Files.readString(p), Object.class); - } - } - return JSON.readValue(arg, Object.class); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineRepl.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineRepl.java deleted file mode 100644 index e2a76c906e..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/RunEngineRepl.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.phoebus.applications.queueserver.Preferences; -import org.phoebus.applications.queueserver.client.*; -import org.phoebus.applications.queueserver.client.ApiEndpoint; -import org.phoebus.applications.queueserver.client.RunEngineHttpClient; -import org.phoebus.applications.queueserver.client.RunEngineHttpClient.*; - - import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.util.Arrays; -import java.util.Map; - -public final class RunEngineRepl { - - private static final String PROMPT = "> "; - private static final ObjectMapper JSON = new ObjectMapper(); - - public static void main(String[] args) throws Exception { - - /* ---- bootstrap client ---- */ - RunEngineHttpClient.initialize(Preferences.queue_server_url, Preferences.api_key); - var client = RunEngineHttpClient.get(); - var console = new BufferedReader(new InputStreamReader(System.in)); - - System.out.println(""" - REPL – type an endpoint (HELP for list), optional JSON body. - Type EXIT or QUIT to leave. Press Enter on an empty line to re-prompt. - """); - - while (true) { - System.out.print(PROMPT); - String line = console.readLine(); - - if (line == null) break; // EOF (Ctrl-D) - if (line.isBlank()) continue; // re-prompt - if ("exit".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line)) break; - - /* ---- HELP ---- */ - if ("help".equalsIgnoreCase(line)) { - Arrays.stream(ApiEndpoint.values()) - .map(Enum::name) - .sorted() - .forEach(System.out::println); - continue; - } - - /* ---- endpoint + optional JSON ---- */ - String[] parts = line.split("\\s+", 2); - ApiEndpoint ep; - try { - ep = ApiEndpoint.valueOf(parts[0].toUpperCase()); - } catch (IllegalArgumentException badName) { - System.out.println("❌ Unknown endpoint. Type HELP for the list."); - continue; - } - - Object body = null; - if (parts.length == 2) { - try { - body = JSON.readValue(parts[1], Object.class); - } catch (JsonProcessingException badJson) { - System.out.println("❌ Invalid JSON: " + badJson.getOriginalMessage()); - continue; - } - } - - try { - Map out = client.send(ep, body); - System.out.println(JSON.writerWithDefaultPrettyPrinter().writeValueAsString(out)); - } catch (BlueskyException be) { - System.out.println("❌ " + be.getMessage()); - } catch (Exception ex) { - System.out.println("❌ Unexpected error: " + ex); - } - } - System.out.println("Bye."); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java deleted file mode 100644 index ad1c1b5dec..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.phoebus.applications.queueserver.util; - -import org.phoebus.applications.queueserver.api.StatusResponse; -import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ChangeListener; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A single instance that always holds the most-recent /api/status result - * (or {@code null} when the server is offline). - * - * Widgets that need live status call - *
 StatusBus.latest().addListener((obs,oldVal,newVal) -> { ... }) 
- * or bind directly to the property. - */ -public final class StatusBus { - - private static final Logger logger = Logger.getLogger(StatusBus.class.getPackageName()); - private static final ObjectProperty LATEST = new SimpleObjectProperty<>(null); - private static final List> listeners = new ArrayList<>(); - - private StatusBus() {} - - public static ObjectProperty latest() { - return LATEST; - } - - /** - * Add a listener and track it for cleanup during reset. - * Use this instead of latest().addListener() for proper lifecycle management. - */ - public static void addListener(ChangeListener listener) { - listeners.add(listener); - LATEST.addListener(listener); - } - - /** - * Remove a tracked listener. - */ - public static void removeListener(ChangeListener listener) { - listeners.remove(listener); - LATEST.removeListener(listener); - } - - public static void push(StatusResponse s) { - if (s != null) { - logger.log(Level.FINEST, "Status update: " + s.managerState()); - } else { - logger.log(Level.FINE, "Status cleared (server offline)"); - } - - if (Platform.isFxApplicationThread()) { - LATEST.set(s); - } else { - Platform.runLater(() -> LATEST.set(s)); - } - } - - /** Reset state for app restart - clears listeners first, then value (to avoid firing listeners during shutdown) */ - public static void reset() { - Runnable doReset = () -> { - // Remove all tracked listeners FIRST to prevent them from firing when we set null - for (ChangeListener listener : listeners) { - LATEST.removeListener(listener); - } - listeners.clear(); - // Now safe to set null - no listeners will fire - LATEST.set(null); - }; - - if (Platform.isFxApplicationThread()) { - doReset.run(); - } else { - Platform.runLater(doReset); - } - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java deleted file mode 100644 index 860c74b705..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.phoebus.applications.queueserver.view; - -import org.phoebus.applications.queueserver.api.QueueItem; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.logging.Logger; -import java.util.logging.Level; - -public class ItemUpdateEvent { - private static final ItemUpdateEvent instance = new ItemUpdateEvent(); - private final List> listeners = new ArrayList<>(); - private static final Logger logger = Logger.getLogger(ItemUpdateEvent.class.getPackageName()); - - private ItemUpdateEvent() {} - - public static ItemUpdateEvent getInstance() { - return instance; - } - - public void addListener(Consumer listener) { - listeners.add(listener); - } - - public void notifyItemUpdated(QueueItem updatedItem) { - for (Consumer listener : listeners) { - try { - listener.accept(updatedItem); - } catch (Exception e) { - logger.log(Level.WARNING, "Item update listener error", e); - } - } - } - - /** Reset state for app restart */ - public void reset() { - listeners.clear(); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java deleted file mode 100644 index b175f474eb..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.phoebus.applications.queueserver.view; - -import org.phoebus.applications.queueserver.api.QueueItem; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class PlanEditEvent { - - private static final PlanEditEvent INSTANCE = new PlanEditEvent(); - - private final List> listeners = new CopyOnWriteArrayList<>(); - private static final Logger logger = Logger.getLogger(PlanEditEvent.class.getPackageName()); - - private PlanEditEvent() {} - - public static PlanEditEvent getInstance() { - return INSTANCE; - } - - public void addListener(Consumer listener) { - listeners.add(listener); - } - - public void removeListener(Consumer listener) { - listeners.remove(listener); - } - - public void notifyEditRequested(QueueItem itemToEdit) { - for (Consumer listener : listeners) { - try { - listener.accept(itemToEdit); - } catch (Exception e) { - logger.log(Level.WARNING, "Error in plan edit listener", e); - } - } - } - - /** Reset state for app restart */ - public void reset() { - listeners.clear(); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java deleted file mode 100644 index 99e2f11a4d..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.phoebus.applications.queueserver.view; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.logging.Logger; -import java.util.logging.Level; - -public class TabSwitchEvent { - private static final TabSwitchEvent instance = new TabSwitchEvent(); - private final List> listeners = new ArrayList<>(); - private static final Logger logger = Logger.getLogger(TabSwitchEvent.class.getPackageName()); - - private TabSwitchEvent() {} - - public static TabSwitchEvent getInstance() { - return instance; - } - - public void addListener(Consumer listener) { - listeners.add(listener); - } - - public void switchToTab(String tabName) { - for (Consumer listener : listeners) { - try { - listener.accept(tabName); - } catch (Exception e) { - logger.log(Level.WARNING, "Tab switch listener error", e); - } - } - } - - /** Reset state for app restart */ - public void reset() { - listeners.clear(); - } -} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/UiSignalEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/UiSignalEvent.java deleted file mode 100644 index 423f0fb02a..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/UiSignalEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.phoebus.applications.queueserver.view; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; - -public final class UiSignalEvent { - - private static final BooleanProperty ENV_DESTROY_ARMED = - new SimpleBooleanProperty(false); - - private UiSignalEvent() {} - - public static BooleanProperty envDestroyArmedProperty() { - return ENV_DESTROY_ARMED; - } - - /** Reset state for app restart */ - public static void reset() { - ENV_DESTROY_ARMED.set(false); - } -} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java deleted file mode 100644 index 2c197c34bd..0000000000 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.phoebus.applications.queueserver.view; - -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.control.Label; -import javafx.scene.layout.StackPane; - -import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Centralised FXML loader. Add new enum constants for every view you create and - * call {@code get()} to obtain the root node. - * - * FXML files themselves continue to live under {@code resources/view}. - */ -public enum ViewFactory { - - APPLICATION ("/org/phoebus/applications/queueserver/view/Application.fxml"), - MONITOR_QUEUE ("/org/phoebus/applications/queueserver/view/MonitorQueue.fxml"), - EDIT_AND_CONTROL_QUEUE ("/org/phoebus/applications/queueserver/view/EditAndControlQueue.fxml"); - - private static final Logger logger = Logger.getLogger(ViewFactory.class.getPackageName()); - - private final String path; - - ViewFactory(String path) { this.path = path; } - - public Parent get() { - try { - return FXMLLoader.load(ViewFactory.class.getResource(path)); - } catch (IOException ex) { - logger.log(Level.SEVERE, "FXML load failed: " + path, ex); - return new StackPane(new Label("⚠ Unable to load " + path)); - } - } - -} diff --git a/app/queue-server/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor b/app/queue-server/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor deleted file mode 100644 index 62a026f374..0000000000 --- a/app/queue-server/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor +++ /dev/null @@ -1 +0,0 @@ -org.phoebus.applications.queueserver.QueueServerApp \ No newline at end of file diff --git a/app/queue-server/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry b/app/queue-server/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry deleted file mode 100644 index c83eb257e1..0000000000 --- a/app/queue-server/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry +++ /dev/null @@ -1 +0,0 @@ -org.phoebus.applications.queueserver.QueueServerMenuEntry \ No newline at end of file diff --git a/app/queue-server/src/main/resources/icons/bluesky.png b/app/queue-server/src/main/resources/icons/bluesky.png deleted file mode 100644 index 6e28c86362c15d304bb6ea1bf405f9aa1fd0bd7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 484 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8lL0;ds5HM@jta)G%5D*X<83|;DgoHo@r%s&;RCDCW zkz>b>ty{Nl!h{JgUc3M*017^T{=A`~;nuBNMMXuECr_R+W5)mg|GT=nva+(u%gdiU zd9rrx+8Z}+95`?wK0f~7!GkF&DVsKJ+Oua*b#--ndwXSNWp;LUUteEqYinwA9wt z78DdDCnuLBUA6*ximxQdFPOpm$h1tJ>4(nmG?+GR+FQ11CxJ54JzX3_BrfZoKPtv# zDByDOft(Rfk73j9-M9bOi(K{G|K{CFSND#tT&=WXr*g~Pyr*Pu&opu@2wA*t+2yZc ztUrRjS$UpLVGI*uJte}ljAh@ZlKcke4FRQh*$tF$d{APmVDXNtVJv297Z*(MFnD;B z|AKl)c?aO*>`{nzm_P>4lduQ`6zvR14?ce1}p@@&|OEZ8@ OWbkzLb6Mw<&;$U2q1%rD diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css deleted file mode 100644 index 6e020b588b..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css +++ /dev/null @@ -1,46 +0,0 @@ -.tab-pane .tab-header-area .tab-header-background { - -fx-opacity: 0; -} - -.tab-pane .tab-content-area { - -fx-border-color: #b0b0b0; - -fx-border-width: 1; - -fx-background-color: #ffffff; - -fx-padding: 8; -} - -/* Completely hide all focus indicators without changing appearance */ -* { - -fx-focus-color: transparent; - -fx-faint-focus-color: transparent; -} - -/* Keep TextArea border same as unfocused when focused */ -.text-area:focused { - -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), - derive(-fx-base,-1%); -} - -/* Override TextArea content focus styling to match unfocused state */ -.text-area:focused .content { - -fx-background-color: - linear-gradient(from 0px 0px to 0px 4px, derive(-fx-control-inner-background, -8%), -fx-control-inner-background); - -fx-background-insets: 0; - -fx-background-radius: 2; -} - -/* Override TableView focus styling */ -.table-view:focused { - -fx-background-color: -fx-box-border, -fx-control-inner-background; - -fx-background-insets: 0, 1; -} - -/* Make table row selection blue instead of gray */ -.table-row-cell:selected { - -fx-background-color: #0096C9; - -fx-table-cell-border-color: derive(#0096C9, 20%); -} - -.table-row-cell:selected .table-cell { - -fx-text-fill: white; -} \ No newline at end of file diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/logging.properties b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/logging.properties deleted file mode 100644 index c14b2fddfd..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/logging.properties +++ /dev/null @@ -1,7 +0,0 @@ -uhandlers = java.util.logging.ConsoleHandler -.level = INFO - -org.phoebus.applications.bluesky.level = FINE - -java.util.logging.ConsoleHandler.level = FINE -java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages.properties b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages.properties deleted file mode 100644 index fcb00c0408..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages.properties +++ /dev/null @@ -1,2 +0,0 @@ -QueueServer=Queue Server -QueueServerMenuPath=Utility \ No newline at end of file diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages_fr.properties b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages_fr.properties deleted file mode 100644 index 2c49330622..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/messages_fr.properties +++ /dev/null @@ -1,2 +0,0 @@ -QueueServer=Serveur de file d’attente -QueueServerMenuPath=Outil diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/scripts/type_converter.py b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/scripts/type_converter.py deleted file mode 100644 index ff485ad9f6..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/scripts/type_converter.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -Type converter script for Queue Server parameter parsing. - -This script uses Python's ast.literal_eval to safely parse parameter values -from string representations, matching the behavior of the Python Qt implementation. - -Usage from Java: - - Pass 'parameters_json' as a JSON string containing parameter definitions - - Returns 'result' as a JSON string with parsed values -""" - -import ast -import json -import sys - -# Sentinel value for empty/unset parameters (Jython 2.7 doesn't have inspect.Parameter.empty) -EMPTY = object() - -def parse_literal_value(value_str): - """ - Parse a string representation of a value into its proper Python type. - Mimics ast.literal_eval behavior used in the PyQt implementation. - - Args: - value_str: String representation of the value - - Returns: - Parsed value with appropriate Python type - """ - if value_str is None or value_str == '': - return EMPTY - - value_str = value_str.strip() - - # Handle None/null - if value_str in ('None', 'null'): - return None - - # Handle booleans - if value_str in ('True', 'true'): - return True - if value_str in ('False', 'false'): - return False - - # Handle quoted strings - if ((value_str.startswith("'") and value_str.endswith("'")) or - (value_str.startswith('"') and value_str.endswith('"'))): - return value_str[1:-1] - - # Try numeric parsing - try: - if '.' in value_str: - return float(value_str) - else: - return int(value_str) - except ValueError: - pass - - # Try ast.literal_eval for lists, dicts, tuples, etc. - try: - return ast.literal_eval(value_str) - except (ValueError, SyntaxError): - # If all else fails, return as string - return value_str - - -def convert_parameters(parameters_data): - """ - Convert parameter values from string representations to typed objects. - - Args: - parameters_data: List of parameter dictionaries with 'name', 'value', 'enabled' fields - - Returns: - Dictionary mapping parameter names to their parsed values - """ - result = {} - - for param in parameters_data: - param_name = param.get('name') - param_value = param.get('value') - is_enabled = param.get('enabled', True) - default_value = param.get('defaultValue') - - # Skip disabled parameters entirely - they should not be included in kwargs - if not is_enabled: - continue - - # Skip parameters with no value (empty and no default) - if param_value is None or param_value == '': - continue - - try: - # Parse the value string to proper type - parsed_value = parse_literal_value(param_value) - - # Only include if it's not empty - if parsed_value != EMPTY: - result[param_name] = parsed_value - - except Exception as e: - # Log error but continue processing other parameters - sys.stderr.write("Warning: Failed to parse parameter '%s' with value '%s': %s\n" % (param_name, param_value, e)) - # Fall back to string value - result[param_name] = param_value - - return result - - -def validate_parameters(parameters_data): - """ - Validate parameter values and return validation results. - - Args: - parameters_data: List of parameter dictionaries - - Returns: - Dictionary with validation results for each parameter - """ - validation_results = {} - - for param in parameters_data: - param_name = param.get('name') - param_value = param.get('value') - is_enabled = param.get('enabled', True) - is_optional = param.get('isOptional', False) - - if not is_enabled: - validation_results[param_name] = {'valid': True, 'message': 'Disabled'} - continue - - if param_value is None or param_value == '': - is_valid = is_optional - validation_results[param_name] = { - 'valid': is_valid, - 'message': 'Required parameter missing' if not is_valid else 'OK' - } - continue - - try: - # Try to parse the value - parse_literal_value(param_value) - validation_results[param_name] = {'valid': True, 'message': 'OK'} - except Exception as e: - validation_results[param_name] = { - 'valid': False, - 'message': 'Parse error: %s' % str(e) - } - - return validation_results - - -# Main execution -if __name__ == '__main__': - # When run directly (for testing) - test_params = [ - {'name': 'detector', 'value': "'det1'", 'enabled': True, 'isOptional': False}, - {'name': 'num_points', 'value': '10', 'enabled': True, 'isOptional': False}, - {'name': 'exposure', 'value': '0.5', 'enabled': True, 'isOptional': False}, - {'name': 'metadata', 'value': "{'key': 'value'}", 'enabled': True, 'isOptional': True}, - ] - - result = convert_parameters(test_params) - # Don't print - just for testing - pass - -# Script entry point for Jython execution -# Expects: parameters_json (input), sets: result (output) -try: - if 'parameters_json' in dir(): - # Parse input JSON - params_data = json.loads(parameters_json) - - # Convert parameters - converted = convert_parameters(params_data) - - # Set result as JSON string - result = json.dumps(converted) - -except Exception as e: - # Return error as result - result = json.dumps({ - 'error': str(e), - 'type': type(e).__name__ - }) - sys.stderr.write("Error in type_converter.py: %s\n" % e) diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml deleted file mode 100644 index 609601d802..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/EditAndControlQueue.fxml b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/EditAndControlQueue.fxml deleted file mode 100644 index 5d94bedfed..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/EditAndControlQueue.fxml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/MonitorQueue.fxml b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/MonitorQueue.fxml deleted file mode 100644 index db7e5ec80e..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/MonitorQueue.fxml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReConsoleMonitor.fxml b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReConsoleMonitor.fxml deleted file mode 100644 index 2857ff8b22..0000000000 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/ReConsoleMonitor.fxml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - -