From 7ca5b40fee520216666d3d1582f524f29dc68773 Mon Sep 17 00:00:00 2001 From: Fanny Rodolakis Date: Tue, 6 Jan 2026 14:02:27 -0600 Subject: [PATCH 1/4] Fix run selection persistence when new runs are added When new runs are added to the catalog, the table view's selection was staying on the same row index instead of tracking the selected run by UID. This caused the visual selection to appear on the wrong run after the table data shifted. Changes: - Restore selection in updateModelData() based on selected_run_uid after model updates, ensuring the correct run remains selected even when new runs are added and rows shift - Move selected_run_uid assignment before try/except blocks in doRunSelectedSlot() and doRunDoubleClickSlot() to ensure it's set even when getDataDescription() fails (e.g., for runs without fields yet). This prevents selection from jumping back to the previous run when selecting runs that don't have plottable data yet. --- gemviz/bluesky_runs_catalog.py | 5 ++++- gemviz/bluesky_runs_catalog_table_view.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/gemviz/bluesky_runs_catalog.py b/gemviz/bluesky_runs_catalog.py index a6bd3dc..78ae484 100644 --- a/gemviz/bluesky_runs_catalog.py +++ b/gemviz/bluesky_runs_catalog.py @@ -242,6 +242,8 @@ def doRunSelectedSlot(self, run): logger.info(f"Refreshing active run {run.uid[:7]} to get latest data") # Note: is_active refreshes metadata + self.selected_run_uid = run.get_run_md("start", "uid") + run_md = run.run_md self.brc_run_viz.setMetadata(yaml.dump(dict(run_md), indent=4)) try: @@ -252,7 +254,6 @@ def doRunSelectedSlot(self, run): ) return self.setStatus(run.summary()) - self.selected_run_uid = run.get_run_md("start", "uid") # Clear reference to old widget (if any) self.current_field_widget = None @@ -291,6 +292,8 @@ def doRunDoubleClickSlot(self, run): logger.info(f"Refreshing active run {run.uid[:7]} to get latest data") # Note: is_active refreshes metadata + self.selected_run_uid = run.get_run_md("start", "uid") + run_md = run.run_md self.brc_run_viz.setMetadata(yaml.dump(dict(run_md), indent=4)) try: diff --git a/gemviz/bluesky_runs_catalog_table_view.py b/gemviz/bluesky_runs_catalog_table_view.py index 5e6fe92..40b8a02 100644 --- a/gemviz/bluesky_runs_catalog_table_view.py +++ b/gemviz/bluesky_runs_catalog_table_view.py @@ -217,6 +217,14 @@ def updateModelData(self): # Send the page of runs to the model now. self.model.setRuns(page) + # Restore selection based on selected_run_uid + selected_uid = self.parent.selected_run_uid + if selected_uid and selected_uid in page: + row_index = list(page.keys()).index(selected_uid) + self.tableView.selectRow(row_index) + elif selected_uid: + self.tableView.clearSelection() + def setPagerStatus(self, text=None): if text is None: total = self.catalogLength() # filtered catalog From de647cae5ca8c77ce710131689454681fca0c76a Mon Sep 17 00:00:00 2001 From: Fanny Rodolakis Date: Tue, 6 Jan 2026 15:24:28 -0600 Subject: [PATCH 2/4] Fix empty field table flashing during live plotting During live plotting updates, refreshFieldData() was rebuilding the field selection table even when fields weren't readable yet (Container objects without .read() method), causing the table to flash empty. Fixed by checking if any fields have readable (non-empty) shapes before rebuilding the table in setStream(); if all fields have empty shapes and an existing table exists, skip the rebuild to prevent the flash. Also changed log level from WARNING to DEBUG for Container objects without .read() method. --- gemviz/select_stream_fields.py | 37 ++++++++++++++++++++++++++++++++-- gemviz/tapi.py | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/gemviz/select_stream_fields.py b/gemviz/select_stream_fields.py index 59dfc82..4d14e87 100644 --- a/gemviz/select_stream_fields.py +++ b/gemviz/select_stream_fields.py @@ -84,9 +84,29 @@ def setStream(self, stream_name): x_names = self.analysis["plot_axes"] y_name = self.analysis["plot_signal"] + # Check if we have an existing table with fields + has_existing_table = ( + self.table_view is not None + and hasattr(self.table_view, "tableView") + and self.table_view.tableView.model() is not None + and len(self.table_view.tableView.model().fields()) > 0 + ) + # describe the data fields for the dialog. - sdf = self.run.stream_data_fields(stream_name) - # print(f"{__name__}.{__class__.__name__}: {sdf=}") + try: + sdf = self.run.stream_data_fields(stream_name) + # print(f"{__name__}.{__class__.__name__}: {sdf=}") + except Exception as exc: + # If we can't get fields and we have an existing table, skip rebuild + if has_existing_table: + logger.debug( + f"Could not get fields for {stream_name}: {exc}, " + "skipping rebuild to avoid empty flash" + ) + return + # If no existing table, we need to try building one + sdf = [] + fields = [] for field_name in sdf: selection = None @@ -107,6 +127,18 @@ def setStream(self, stream_name): fields.append(field) logger.debug("fields=%s", fields) + # Check if any fields have valid (non-empty) shapes + # If all fields have empty shapes, they're not readable yet (Container objects) + has_readable_fields = any(len(field.shape) > 0 for field in fields) + + # If no readable fields AND we have an existing table, skip rebuild + if not has_readable_fields and has_existing_table: + logger.debug( + f"No readable fields available for {stream_name} (all have empty shapes), " + "skipping rebuild to avoid empty flash" + ) + return + # build the view of this stream view = SelectFieldsTableView(self) self.table_view = view @@ -142,6 +174,7 @@ def refreshFieldData(self): saved = {} # Rebuild the table for the current stream using fresh data. + # setStream() will check if fields are readable before rebuilding current_stream = self.stream_name self.setStream(current_stream) diff --git a/gemviz/tapi.py b/gemviz/tapi.py index 152c116..54c8d00 100644 --- a/gemviz/tapi.py +++ b/gemviz/tapi.py @@ -366,7 +366,7 @@ def _read_stream_arrays(self, stream_name): data = data_node[field].read() except (KeyError, AttributeError) as exc: # Field doesn't exist yet - skip it - logger.warning( + logger.debug( f"Field {field} not yet available for {stream_name}: {exc}" ) continue From 707da1a2ee8ea2499361b632513edc57c8e4cf2d Mon Sep 17 00:00:00 2001 From: Fanny Rodolakis Date: Tue, 6 Jan 2026 17:31:10 -0600 Subject: [PATCH 3/4] Catch httpx.ConnectError in stream_data and log as warning Prevents traceback flood when tiled server connection fails after stamina retries. --- gemviz/tapi.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gemviz/tapi.py b/gemviz/tapi.py index 54c8d00..a5201cb 100644 --- a/gemviz/tapi.py +++ b/gemviz/tapi.py @@ -16,7 +16,7 @@ import numpy import tiled import tiled.queries -from httpx import HTTPStatusError +from httpx import ConnectError, HTTPStatusError logger = logging.getLogger(__name__) @@ -254,6 +254,9 @@ def stream_data(self, stream_name): else: logger.error(f"Failed to read stream data for {stream_name}: {exc}") raise + except ConnectError as exc: + logger.warning(f"Connection error reading stream {stream_name}: {exc}") + raise except Exception as exc: logger.error(f"Error reading stream data for {stream_name}: {exc}") raise From 4d03cb0434ef8d71f152858631d6a15ef96c2830 Mon Sep 17 00:00:00 2001 From: Fanny Rodolakis Date: Wed, 7 Jan 2026 11:33:27 -0600 Subject: [PATCH 4/4] Remove duplicate selected_run_uid assignment in doRunDoubleClickSlot The selected_run_uid is already set before the try/except block (line 295), so the duplicate assignment after setStatus() (line 306) was redundant. --- gemviz/bluesky_runs_catalog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gemviz/bluesky_runs_catalog.py b/gemviz/bluesky_runs_catalog.py index 78ae484..338d0da 100644 --- a/gemviz/bluesky_runs_catalog.py +++ b/gemviz/bluesky_runs_catalog.py @@ -304,7 +304,6 @@ def doRunDoubleClickSlot(self, run): ) return self.setStatus(run.summary()) - self.selected_run_uid = run.get_run_md("start", "uid") # Clear reference to old widget (if any) self.current_field_widget = None