From 462e8199409c3c68cfc3783c47f88e992c44c451 Mon Sep 17 00:00:00 2001 From: FLorial Jean Baptiste Date: Tue, 23 Jun 2026 11:12:24 +0200 Subject: [PATCH 1/3] [Harvester] Remove manual "Send Data to Crims" trigger CRIMS upload is now sent automatically when a data collection finishes (see commit 2), making the manual button and its route not needed. --- mxcubeweb/routes/harvester.py | 9 --------- ui/src/actions/harvester.js | 16 ---------------- ui/src/api/harvester.js | 4 ---- .../Equipment/HarvesterMaintenance.jsx | 14 -------------- 4 files changed, 43 deletions(-) diff --git a/mxcubeweb/routes/harvester.py b/mxcubeweb/routes/harvester.py index 7995bf9b4..a7cd360d5 100644 --- a/mxcubeweb/routes/harvester.py +++ b/mxcubeweb/routes/harvester.py @@ -81,15 +81,6 @@ def calibrate(): return jsonify(app.harvester.get_harvester_contents()) return None - @bp.route("/send_data_collection_info_to_crims", methods=["GET"]) - @server.require_control - @server.restrict - def send_data_collection_info_to_crims(): - ret = app.harvester.send_data_collection_info_to_crims() - if ret: - return jsonify(app.harvester.get_harvester_contents()) - return None - @bp.route("/validate_calibration", methods=["POST"]) @server.restrict def validate_calibration(): diff --git a/ui/src/actions/harvester.js b/ui/src/actions/harvester.js index bc723e344..e51441277 100644 --- a/ui/src/actions/harvester.js +++ b/ui/src/actions/harvester.js @@ -1,7 +1,6 @@ import { sendAbortHarvester, sendCalibratePin, - sendDataCollectionInfoToCrims, sendHarvestAndLoadCrystal, sendHarvestCrystal, sendHarvesterCommand, @@ -72,21 +71,6 @@ export function harvestAndLoadCrystal(xtalUUID, successCb = null) { }; } -export function sendDataCollectionToCrims() { - return async (dispatch) => { - try { - const contents = await sendDataCollectionInfoToCrims(); - dispatch(setContents(contents)); - - // temporary use ErrorPanel to display success message - dispatch(showErrorPanel(true, 'Succesfully sent DC to Crims')); - } catch (error) { - dispatch(showErrorPanel(true, error.response.headers.get('message'))); - throw new Error('Server refused to send DC to Crims'); - } - }; -} - export function calibratePin(successCb = null) { return async (dispatch) => { try { diff --git a/ui/src/api/harvester.js b/ui/src/api/harvester.js index 439973083..9b463c1b1 100644 --- a/ui/src/api/harvester.js +++ b/ui/src/api/harvester.js @@ -24,10 +24,6 @@ export function sendCalibratePin() { return endpoint.get('/calibrate').safeJson(); } -export function sendDataCollectionInfoToCrims() { - return endpoint.get('/send_data_collection_info_to_crims').safeJson(); -} - export function sendValidateCalibration(validated) { return endpoint .post(JSON.stringify(validated), '/validate_calibration') diff --git a/ui/src/components/Equipment/HarvesterMaintenance.jsx b/ui/src/components/Equipment/HarvesterMaintenance.jsx index f65fb4089..cf95b60f7 100644 --- a/ui/src/components/Equipment/HarvesterMaintenance.jsx +++ b/ui/src/components/Equipment/HarvesterMaintenance.jsx @@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { calibratePin, sendCommand, - sendDataCollectionToCrims, validateCalibration, } from '../../actions/harvester'; import InOutSwitch from '../InOutSwitch/InOutSwitch'; @@ -126,19 +125,6 @@ export default function HarvesterMaintenance() { )} -
- - - - - From f1b0d911e871a7abd1c5540ad676e869a4f1582c Mon Sep 17 00:00:00 2001 From: FLorial Jean Baptiste Date: Tue, 30 Jun 2026 17:43:19 +0200 Subject: [PATCH 2/3] [Harvester] Auto-send data collection to CRIMS on DC completion Replaces the manual "Send Data to Crims" button with an automatic trigger: the harvester component now listens to queue_entry_execute_finished and calls send_data_collection_info_to_crims after every successful DataCollectionQueueEntry. The manual route, action, and UI button are removed. --- mxcubeweb/core/components/harvester.py | 77 +++++++++++++++----------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/mxcubeweb/core/components/harvester.py b/mxcubeweb/core/components/harvester.py index 43b27534d..3e9ac1961 100644 --- a/mxcubeweb/core/components/harvester.py +++ b/mxcubeweb/core/components/harvester.py @@ -2,9 +2,11 @@ import logging +import gevent from mxcubecore import HardwareRepository as HWR from mxcubecore.HardwareObjects.abstract.sample_changer import Crims from mxcubecore.HardwareObjects.Harvester import HarvesterState +from mxcubecore.queue_entry.data_collection import DataCollectionQueueEntry from mxcubeweb.core.components.component_base import ComponentBase @@ -25,6 +27,10 @@ def init_signals(self): "harvester_contents_update", self.harvester_contents_update ) + HWR.beamline.queue_manager.connect( + "queue_entry_execute_finished", self.queue_entry_execute_finished + ) + def harvester_state_changed(self, *args): new_state = args[0] state_str = HarvesterState.STATE_DESC.get(new_state, "Unknown").upper() @@ -33,6 +39,14 @@ def harvester_state_changed(self, *args): def harvester_contents_update(self): self.app.server.emit("harvester_contents_update") + def queue_entry_execute_finished(self, entry, status): + """Forward a finished data collection to CRIMS, since the sample + came from the Harvester.""" + if not isinstance(entry, DataCollectionQueueEntry) or status != "Successful": + return + + gevent.spawn(self._send_collection_to_crims, entry.get_data_model()) + def get_initial_state(self): if HWR.beamline.harvester_maintenance is not None: ( @@ -139,7 +153,8 @@ def get_crystal_list(self): } crystal_list.append(lst) except Exception: - logging.getLogger("user_level_log").exception("Could not get Crystal List") + logging.getLogger("user_level_log").error("Could not get Crystal List") + logging.getLogger("HWR").exception("") return crystal_list @@ -147,48 +162,46 @@ def get_global_state(self): try: return HWR.beamline.harvester_maintenance.get_global_state() except Exception: - logging.getLogger("user_level_log").exception("Could not get global state") + logging.getLogger("user_level_log").error("Could not get global state") + logging.getLogger("HWR").exception("") return "OFFLINE", "OFFLINE", "OFFLINE" - def send_data_collection_info_to_crims(self) -> bool: - """Send Data collected to CRIMS. + def _send_collection_to_crims(self, data_model) -> None: + """Send a single data collection's info to CRIMS. - Returns: - bool: Whether the request failed (``False``) or not (``True``). + Args: + data_model: The `DataCollection` queue model object to report. """ - dataCollectionGroupId = "" - crystal_uuid = "" - try: - rest_token = HWR.beamline.lims.get_rest_token() - proposal = HWR.beamline.session.get_proposal() - crims_url = self.harvester_device.crims_upload_url crims_key = self.harvester_device.crims_upload_key - queue_entries = HWR.beamline.queue_model.get_all_dc_queue_entries() - dc_id = "" - for qe in queue_entries: - dataCollectionGroupId = qe.get_data_model().lims_group_id - crystal_uuid = ( - qe.get_data_model().get_sample_node().crystals[0].crystal_uuid - ) - dc_id = qe.get_data_model().id - - Crims.send_data_collection_info_to_crims( - crims_url, + crystal_uuid = data_model.get_sample_node().crystals[0].crystal_uuid + sample_name = data_model.get_sample_node().name + instrument_name = HWR.beamline.session.beamline_name + facility_name = HWR.beamline.session.synchrotron_name + investigation_id = HWR.beamline.session.session_id + + sent = Crims.send_data_collection_info_to_crims( + crims_url, + crystal_uuid, + sample_name, + instrument_name, + facility_name, + investigation_id, + crims_key, + ) + if not sent: + logging.getLogger("user_level_log").warning( + "Data collection was not sent to CRIMS: crystal=%s sample=%s", crystal_uuid, - dataCollectionGroupId, - dc_id, - proposal, - rest_token, - crims_key, + sample_name, ) - return True except Exception: - msg = "Could not send data collection to crims" - logging.getLogger("user_level_log").exception(msg) - return False + logging.getLogger("user_level_log").error( + "Could not send data collection to CRIMS" + ) + logging.getLogger("HWR").exception("") def get_sample_by_id(self, sampleID: str): samples_list = HWR.beamline.sample_changer.get_sample_list() From 0a32339d0becbc044901d7398fb1eaec590dbd5e Mon Sep 17 00:00:00 2001 From: FLorial Jean Baptiste Date: Wed, 15 Jul 2026 12:49:00 +0200 Subject: [PATCH 3/3] [deps] Update mxcubecore to 2.99.0 --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index aaae00526..22fabc1d7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2251,14 +2251,14 @@ tango = ["pytango (>=9.3.6,<10.0.0)"] [[package]] name = "mxcubecore" -version = "2.75.0" +version = "2.99.0" description = "Core libraries for the MXCuBE application" optional = false python-versions = "<3.12,>=3.10" groups = ["main"] files = [ - {file = "mxcubecore-2.75.0-py3-none-any.whl", hash = "sha256:1a09403de208e70b918f5a7f8b3abbb6dbeccd5c35873ebcf3dd30079fa841d7"}, - {file = "mxcubecore-2.75.0.tar.gz", hash = "sha256:a9ceb90e221e8f2a598e8a948fbf171894174ab24d782e428d64c2aefce83480"}, + {file = "mxcubecore-2.99.0-py3-none-any.whl", hash = "sha256:2960598a1b817fb319413f3d3a34d054ecc30ddc025066cdaca0a5249620352f"}, + {file = "mxcubecore-2.99.0.tar.gz", hash = "sha256:875c2da1d3b3abd4fb416e5032403522d72b99adf9db78e901dff6174a9c898a"}, ] [package.dependencies] @@ -4573,4 +4573,4 @@ graylog = ["graypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.12" -content-hash = "9b1f89dd14313fce0799fd49c053eb29ea264e88fab3976e78e1069dbcad5b8b" +content-hash = "67aee26c52594735e5a2eac69774959216b4615a15a2c553f60055ba3d2f5c99" diff --git a/pyproject.toml b/pyproject.toml index 440b72e35..68c7eeee4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ Flask-Security = { version = "5.7.1", extras = ["common", "fsqla"] } Flask-SocketIO = "^5.3.6" gevent = "^23.9.1" markupsafe = "^2.1.5" -mxcubecore = ">=2.75.0" +mxcubecore = ">=2.99.0" pydantic = ">=2.8.2,<2.9.0" pytz = "^2022.6" requests = "^2.33.0"