From 2a239823300571d40d62a3c97ba9f7fec6d77ada Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 8 May 2025 19:44:05 +0100 Subject: [PATCH 01/18] Added purge_empty_scans() method --- .../things/smart_scan.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 839d64b5..4919ef3c 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -1141,3 +1141,25 @@ class SmartScanThing(Thing): """List the relative paths of all files and folders in the zip folder specified""" scan_zip = zipfile.ZipFile(zip_path) return [os.path.normpath(i) for i in scan_zip.namelist()] + + @thing_action + def purge_empty_scans(self, logger: InvocationLogger) -> None: + """ + Delete all scan folders containing no images at the top level + """ + scan_list = self.scans + + # Filter out scans with no top level files, ignoring JSON files + # JSON is ignored as it's created before any images are captured + for scan in scan_list: + scan_folder = os.path.join(self.base_scan_dir, scan.name, IMG_DIR_NAME) + image_list = [ + f + for f in os.listdir(scan_folder) + if os.path.isfile(os.path.join(scan_folder, f)) and "json" not in f + ] + + logger.info(image_list) + + if len(image_list) == 0: + self.delete_scan(scan.name) From 0df363a1bec8494387e964581f9521dd5c02b405 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 9 May 2025 12:12:12 +0100 Subject: [PATCH 02/18] Run purge_empty_scans() at the end of each scan --- src/openflexure_microscope_server/things/smart_scan.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 4919ef3c..9c7baa84 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -527,6 +527,8 @@ class SmartScanThing(Thing): ) raise e finally: + # Whether or not this scan succeeded, remove any scan folders containing zero images + self.purge_empty_scans(logger=self._scan_logger) if self._capture_thread: # If the capture thread had an error, we capture it here try: From 3a45c68ef3fed5160debd8532ae5d2ed74b99fd7 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 13 May 2025 12:23:01 +0100 Subject: [PATCH 03/18] Try Except around deleting scans to check for PermissionErrors --- .../things/smart_scan.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 9c7baa84..df14bc2e 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -843,7 +843,7 @@ class SmartScanThing(Thing): "delete", "scans", ) - def delete_all_scans(self) -> None: + def delete_all_scans(self, logger:InvocationLogger) -> None: """Delete all the scans on the microscope **This will irreversibly remove all scanned data from the @@ -851,7 +851,10 @@ class SmartScanThing(Thing): Use with extreme caution. """ for scan in self.scans: - self.delete_scan(scan.name) + try: + self.delete_scan(scan.name) + except PermissionError: + logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") @property def latest_preview_stitch_path(self): @@ -1161,7 +1164,8 @@ class SmartScanThing(Thing): if os.path.isfile(os.path.join(scan_folder, f)) and "json" not in f ] - logger.info(image_list) - if len(image_list) == 0: - self.delete_scan(scan.name) + try: + self.delete_scan(scan.name) + except PermissionError: + logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") From 0e3c71b958a886fbfcbfbaebffb15912cf413e7c Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 13 May 2025 12:36:06 +0100 Subject: [PATCH 04/18] Removed from finally block --- .../things/smart_scan.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index df14bc2e..2a58ba64 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -527,8 +527,6 @@ class SmartScanThing(Thing): ) raise e finally: - # Whether or not this scan succeeded, remove any scan folders containing zero images - self.purge_empty_scans(logger=self._scan_logger) if self._capture_thread: # If the capture thread had an error, we capture it here try: @@ -550,6 +548,9 @@ class SmartScanThing(Thing): self._return_to_starting_position() self._perform_final_stitch() + # Remove any scan folders containing zero images + self.purge_empty_scans(logger=self._scan_logger) + @_scan_running def _main_scan_loop(self): """ @@ -843,7 +844,7 @@ class SmartScanThing(Thing): "delete", "scans", ) - def delete_all_scans(self, logger:InvocationLogger) -> None: + def delete_all_scans(self, logger: InvocationLogger) -> None: """Delete all the scans on the microscope **This will irreversibly remove all scanned data from the @@ -854,7 +855,9 @@ class SmartScanThing(Thing): try: self.delete_scan(scan.name) except PermissionError: - logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") + logger.warning( + f"Could not delete scan {scan.name}. Check folder permissions" + ) @property def latest_preview_stitch_path(self): @@ -1168,4 +1171,6 @@ class SmartScanThing(Thing): try: self.delete_scan(scan.name) except PermissionError: - logger.warning(f"Could not delete scan {scan.name}. Check folder permissions") + logger.warning( + f"Could not delete scan {scan.name}. Check folder permissions" + ) From 19d72bef1cefe4793e63766af4749c78de1346b8 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Wed, 14 May 2025 16:11:02 +0100 Subject: [PATCH 05/18] Error handling in _delete_scan(), which is now handled in GUI main.js --- .../things/smart_scan.py | 34 +++++++++++-------- webapp/src/main.js | 10 ++++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 2a58ba64..a3c11eed 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -829,7 +829,7 @@ class SmartScanThing(Thing): 404: {"description": "Scan not found"}, }, ) - def delete_scan(self, scan_name: str) -> None: + def delete_scan(self, scan_name: str, logger: InvocationLogger) -> None: """Delete all files from a scan. This endpoint allows scans to be deleted from disk. @@ -837,8 +837,10 @@ class SmartScanThing(Thing): path = os.path.join(self.base_scan_dir, scan_name) if not os.path.isdir(path): print(f"can't find {path}") - raise HTTPException(404, "Scan not found") - shutil.rmtree(path) + raise HTTPException(400, "Scan not found") + result = self._delete_scan(path, logger) + if not result: + raise HTTPException(400, "Couldn't delete scan, check log for details") @fastapi_endpoint( "delete", @@ -852,12 +854,19 @@ class SmartScanThing(Thing): Use with extreme caution. """ for scan in self.scans: - try: - self.delete_scan(scan.name) - except PermissionError: - logger.warning( - f"Could not delete scan {scan.name}. Check folder permissions" - ) + self.delete_scan(scan.name, logger) + + @thing_action + def _delete_scan(self, scan_path, logger: InvocationLogger) -> bool: + try: + shutil.rmtree(scan_path) + return True + except Exception as e: + logger.warning( + "Attempted to delete scan " + scan_path + ", which failed." + " Server sent response" + str(e) + ) + return False @property def latest_preview_stitch_path(self): @@ -1168,9 +1177,4 @@ class SmartScanThing(Thing): ] if len(image_list) == 0: - try: - self.delete_scan(scan.name) - except PermissionError: - logger.warning( - f"Could not delete scan {scan.name}. Check folder permissions" - ) + self.delete_scan(scan.name) diff --git a/webapp/src/main.js b/webapp/src/main.js index c95e5869..ed49dbc1 100644 --- a/webapp/src/main.js +++ b/webapp/src/main.js @@ -144,10 +144,16 @@ Vue.mixin({ if (error.response) { // If the response is a nicely formatted JSON response from the server if (error.response.data.message) { - return `${error.response.status}: ${error.response.data.message}`; + return `${error.response.data.message}`; + } + if (error.response.data.detail) { + return `${error.response.data.detail}`; } // If the response is just some generic error response - return `${error.response.status}: ${error.response.data}`; + if (error.response.data){ + return `${error.response.data}`; + } + return `${error.response}`; } // If we have an error object with a message, use that if (error.message) return `${error.message}`; From d4b3508b776f5b35855a7ebc6f20be973a69aa49 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 15 May 2025 14:07:06 +0100 Subject: [PATCH 06/18] Clean up of serve_static_files --- .../server/serve_static_files.py | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/openflexure_microscope_server/server/serve_static_files.py b/src/openflexure_microscope_server/server/serve_static_files.py index 5855d295..c00fda32 100644 --- a/src/openflexure_microscope_server/server/serve_static_files.py +++ b/src/openflexure_microscope_server/server/serve_static_files.py @@ -1,12 +1,11 @@ +import os + from fastapi.responses import FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi import FastAPI -import os -import pathlib def add_static_file(app: FastAPI, fname: str, folder: str): - print(f"Adding route for /{fname}") p = os.path.join(folder, fname) app.get(f"/{fname}", response_class=FileResponse, include_in_schema=False)( lambda: FileResponse(p) @@ -14,28 +13,15 @@ def add_static_file(app: FastAPI, fname: str, folder: str): def add_static_files(app: FastAPI): - # with importlib.resources.as_file(openflexure_microscope_server) as p: - # static_path = p.join("/static/") - # TODO: don't hard code this! - search_paths = [ - "/var/openflexure/application/openflexure-microscope-server/src/openflexure_microscope_server/static", - pathlib.Path().absolute() - / "application/openflexure-microscope-server/src/openflexure_microscope_server/static", - ] - if __file__: - search_paths.append(pathlib.Path(__file__).parent.parent / "static") - - for static_path in search_paths: - if os.path.isdir(static_path): - break # stop once one of the paths exists - else: - # If we get to the else: block, no pat was found. - raise RuntimeError("Can't find static files :(") + static_path = os.path.abspath(os.path.join(__file__, "..", "static")) + if not os.path.isdir(static_path): + raise RuntimeError("Can't find static files") @app.get("/", response_class=RedirectResponse) async def redirect_fastapi(): return "/index.html" + # Mounting the webapp at / file by file to allow other endpoints to be created for fname in os.listdir(static_path): fpath = os.path.join(static_path, fname) if os.path.isfile(fpath): From a563c634a3f522b3758ddbe48ef80482640fa34b Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Thu, 15 May 2025 16:30:37 +0000 Subject: [PATCH 07/18] Update serve static files to serve correct directory and to add docstrings --- .../server/serve_static_files.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/openflexure_microscope_server/server/serve_static_files.py b/src/openflexure_microscope_server/server/serve_static_files.py index c00fda32..d87e5dec 100644 --- a/src/openflexure_microscope_server/server/serve_static_files.py +++ b/src/openflexure_microscope_server/server/serve_static_files.py @@ -4,18 +4,30 @@ from fastapi.responses import FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi import FastAPI +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -def add_static_file(app: FastAPI, fname: str, folder: str): + +def add_static_file(app: FastAPI, fname: str, folder: str) -> None: + """Add a single file to the root of the FastAPI app + The file with name `fname` will be mounted at `/fname` - the + `folder` does not affect where it is mounted in the app. + + app: The FastAPI app to add to, in this case the OpenFlexure server + fname: the name of the file to add + folder: the containing folder of the file + """ p = os.path.join(folder, fname) app.get(f"/{fname}", response_class=FileResponse, include_in_schema=False)( lambda: FileResponse(p) ) -def add_static_files(app: FastAPI): - static_path = os.path.abspath(os.path.join(__file__, "..", "static")) - if not os.path.isdir(static_path): - raise RuntimeError("Can't find static files") +def add_static_files(app: FastAPI) -> None: + """Add the static files responsible for the webapp app to the FastAPI app + + app: The FastAPI app to add to, in this case the OpenFlexure server + """ + static_path = os.path.normpath(os.path.join(THIS_DIR, "..", "static")) @app.get("/", response_class=RedirectResponse) async def redirect_fastapi(): From 8c7b800983e621bdc4027e8eca0dd1bb4f19db97 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 16 May 2025 15:18:33 +0100 Subject: [PATCH 08/18] Update theme.less to prevent automatic changed colour of uk-alert-success --- webapp/src/assets/less/theme.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webapp/src/assets/less/theme.less b/webapp/src/assets/less/theme.less index df88729b..a2a1dfcf 100644 --- a/webapp/src/assets/less/theme.less +++ b/webapp/src/assets/less/theme.less @@ -343,6 +343,11 @@ a:hover { a:hover { color: @inverse-primary-muted-color; } + + .uk-alert-success{ + color: rgba(28, 131, 45); + background-color: rgba(130, 221, 145, 0.671); + } } /* From 625f9b40015895f65abf4510a47d066e16753464 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Tue, 20 May 2025 12:59:49 +0100 Subject: [PATCH 09/18] Ttile case in all buttons and properties, larger settings headers --- webapp/src/assets/less/theme.less | 7 ++++++- webapp/src/components/appContent.vue | 10 +++++----- .../aboutComponents/devTools.vue | 6 ------ .../aboutComponents/statusPane.vue | 6 +++--- .../paneBackgroundDetect.vue | 8 ++++---- .../settingsComponents/CSMSettings.vue | 4 ++-- .../CSMCalibrationSettings.vue | 4 ++-- .../settingsComponents/cameraSettings.vue | 10 +++++----- .../cameraCalibrationSettings.vue | 10 +++++----- .../settingsComponents/streamSettings.vue | 4 ++-- .../tabContentComponents/settingsContent.vue | 9 ++++----- .../tabContentComponents/slideScanContent.vue | 14 +++++++------- 12 files changed, 45 insertions(+), 47 deletions(-) diff --git a/webapp/src/assets/less/theme.less b/webapp/src/assets/less/theme.less index a2a1dfcf..a8c6e80f 100644 --- a/webapp/src/assets/less/theme.less +++ b/webapp/src/assets/less/theme.less @@ -80,7 +80,11 @@ // Nav // -@nav-header-font-size: 12px; +@nav-header-font-size: 16px; +.uk-nav-header{ + text-transform:none; +} + // // Subnav @@ -358,6 +362,7 @@ a:hover { padding: 0 8px; border-color: @global-border; margin-bottom: 2px; + text-transform:none !important; } .uk-button-default { diff --git a/webapp/src/components/appContent.vue b/webapp/src/components/appContent.vue index 2662eebc..fcbf7296 100644 --- a/webapp/src/components/appContent.vue +++ b/webapp/src/components/appContent.vue @@ -205,27 +205,27 @@ export default { topTabs: function() { let tabs = [ { - id: "view", + id: "View", icon: "visibility", component: viewContent }, { - id: "navigate", + id: "Navigate", icon: "gamepad", component: navigateContent }, { - id: "background detect", + id: "Background Detect", icon: "background_replace", component: backgroundDetectContent }, { - id: "slide scan", + id: "Slide Scan", icon: "settings_overscan", component: slideScanContent }, { - id: "scan list", + id: "Scan List", icon: "photo_library", component: ScanListContent } diff --git a/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue b/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue index 0bcc9cb5..0c17a105 100644 --- a/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue +++ b/webapp/src/components/tabContentComponents/aboutComponents/devTools.vue @@ -25,12 +25,6 @@ Apply -
- - -
diff --git a/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue b/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue index dc34291f..e3ca4f35 100644 --- a/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue +++ b/webapp/src/components/tabContentComponents/aboutComponents/statusPane.vue @@ -3,12 +3,12 @@
- Microscope hostname: + Microscope Hostname:
{{ $store.state.microscopeHostname }}
- API origin: + API Origin:
{{ $store.state.origin }}
@@ -24,7 +24,7 @@
- Server version:
+ Server Version:
TODO
diff --git a/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue b/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue index 11c4cc64..8d6c8bac 100644 --- a/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue +++ b/webapp/src/components/tabContentComponents/backgroundDetectComponents/paneBackgroundDetect.vue @@ -19,14 +19,14 @@
-

Camera/stage mapping

+

Camera to Stage Mapping

- Camera/stage mapping allows the stage to move relative to the camera + Camera-stage mapping allows the stage to move relative to the camera view. This enables functions like click-to-move, and more precise tile scans.

diff --git a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue index 5c9d02c4..8c892057 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue @@ -11,7 +11,7 @@ " thing="camera_stage_mapping" action="calibrate_xy" - :submit-label="'Auto-Calibrate using camera'" + :submit-label="'Auto-Calibrate Using Camera'" :modal-progress="true" @response="onRecalibrateResponse" @error="modalError" @@ -24,7 +24,7 @@ class="uk-button uk-button-default uk-width-1-1" @click="getCalibrationData()" > - Download calibration data + Download Calibration Data
diff --git a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue index 2ad51115..69f3f837 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue @@ -13,19 +13,19 @@ Pi Camera Settings
Image Quality
@@ -32,7 +32,7 @@ :requires-confirmation="false" thing="camera" action="calibrate_white_balance" - :submit-label="'Auto white balance'" + :submit-label="'Auto White Balance'" @response="onRecalibrateResponse" @error="modalError" /> @@ -47,7 +47,7 @@ " thing="camera" action="calibrate_lens_shading" - :submit-label="'Auto flat field correction'" + :submit-label="'Auto Flat Field Correction'" @response="onRecalibrateResponse" @error="modalError" /> @@ -63,7 +63,7 @@ :requires-confirmation="false" thing="camera" action="flat_lens_shading" - :submit-label="'Disable flat field correction'" + :submit-label="'Disable Flat Field Correction'" @response="onRecalibrateResponse" @error="modalError" /> @@ -78,7 +78,7 @@ :requires-confirmation="false" thing="camera" action="reset_lens_shading" - :submit-label="'Reset flat field correction'" + :submit-label="'Reset Flat Field Correction'" @response="onRecalibrateResponse" @error="modalError" /> diff --git a/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue index c85ff7d9..b6013c11 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/streamSettings.vue @@ -1,10 +1,10 @@