From 702d18552d6dfcf52429162a5e99931c1024f163 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Fri, 29 May 2026 23:46:48 -0600 Subject: [PATCH 01/10] ui_migration bugfix(component) streamContent.vue, Bugfix resize warns wrong spelling of doneResizing to correct: handleDoneResize and move resizeTimeoutId to mounted --- .../tabContentComponents/streamContent.vue | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/webapp/src/components/tabContentComponents/streamContent.vue b/webapp/src/components/tabContentComponents/streamContent.vue index 753a8fa8..4cbc33cf 100644 --- a/webapp/src/components/tabContentComponents/streamContent.vue +++ b/webapp/src/components/tabContentComponents/streamContent.vue @@ -52,7 +52,7 @@ export default { isVisible: false, displaySize: [0, 0], displayPosition: [0, 0], - resizeTimeoutId: setTimeout(this.doneResizing, 500), + resizeTimeoutId: null, }; }, @@ -71,32 +71,33 @@ export default { }, mounted() { - //set up an intersection observer - useIntersectionObserver( + // Initial Timeout + this.resizeTimeoutId = setTimeout(this.handleDoneResize, 500); + + // set up an intersection observer + // We capture the 'stop' function returned by VueUse so we can kill it later + const { stop } = useIntersectionObserver( this.$refs.streamDisplay, ([{ isIntersecting }]) => { this.isVisible = isIntersecting; }, - { - threshold: 0.0, - }, + { threshold: 0.0 }, ); + this.stopIntersectionObserver = stop; // A global signal listener to flash the stream element - this.onFlashStream = () => { - this.flashStream(); - }; - - eventBus.on("globalFlashStream", this.onFlashStream); + eventBus.on("globalFlashStream", this.flashStream); // Mutation observer this.sizeObserver = new ResizeObserver(() => { this.handleResize(); // For any element attached to the observer, run handleResize() on change }); - // Fetch streamDisplay component by ref - if (this.$refs.streamDisplay && this.$refs.streamDisplay.parentNode) { - const streamDisplayElement = this.$refs.streamDisplay.parentNode; + + // Safely fetch the DOM element (handles both native HTML and Vue components) + const streamElement = this.$refs.streamDisplay?.$el || this.$refs.streamDisplay; + + if (streamElement && streamElement.parentNode) { // Attach streamDisplay to the size observer - this.sizeObserver.observe(streamDisplayElement); + this.sizeObserver.observe(streamElement.parentNode); } }, @@ -104,13 +105,23 @@ export default { // Do nothing: preview stream now runs all the time }, - beforeUnmount: function () { - // Disconnect the size observer - this.sizeObserver.disconnect(); - // Remove from the array of active streams - this.store.removeStream(this.$.uid); - // close signal - eventBus.off("globalFlashStream", this.onFlashStream); + beforeUnmount() { + // Kill the timeout + clearTimeout(this.resizeTimeoutId); + + // Kill the Intersection Observer + if (this.stopIntersectionObserver) { + this.stopIntersectionObserver(); + } + + // Unregister the event bus listener + eventBus.off("globalFlashStream", this.flashStream); + + // Disconnect the size Observer + if (this.sizeObserver) { + this.sizeObserver.disconnect(); + this.store.removeStream(this.$.uid); + } }, methods: { From 60f79a41d8836cde54ff2344670a22cb10ee6601 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Tue, 2 Jun 2026 06:38:11 -0600 Subject: [PATCH 02/10] ui_migration bugfix(component) Added setStreamId prop, img v-show and one pixel fallback to drop jpeg stream connection --- .../tabContentComponents/controlContent.vue | 8 ++- .../tabContentComponents/slideScanContent.vue | 3 +- .../tabContentComponents/streamContent.vue | 56 +++++++++++++++---- .../tabContentComponents/viewContent.vue | 8 ++- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/webapp/src/components/tabContentComponents/controlContent.vue b/webapp/src/components/tabContentComponents/controlContent.vue index 4a75dcd6..522f2297 100644 --- a/webapp/src/components/tabContentComponents/controlContent.vue +++ b/webapp/src/components/tabContentComponents/controlContent.vue @@ -5,7 +5,7 @@
- +
@@ -21,6 +21,12 @@ export default { paneControl, streamDisplay, }, + + data() { + return { + setStreamId: "control", + }; + }, }; diff --git a/webapp/src/components/tabContentComponents/slideScanContent.vue b/webapp/src/components/tabContentComponents/slideScanContent.vue index 6f8255ba..6430a69a 100644 --- a/webapp/src/components/tabContentComponents/slideScanContent.vue +++ b/webapp/src/components/tabContentComponents/slideScanContent.vue @@ -17,7 +17,7 @@ class="image-fit" :src="lastStitchedImage" /> - + @@ -16,5 +16,11 @@ export default { components: { streamDisplay, }, + + data() { + return { + setStreamId: "view", + }; + }, }; From 8aaddf6fb119f62690662e184055196519d29998 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Wed, 3 Jun 2026 06:19:32 -0600 Subject: [PATCH 03/10] ui_migration bugfix(evenBus) Add missing emit for globalflashstream animation on capture --- webapp/src/components/tabContentComponents/controlContent.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/tabContentComponents/controlContent.vue b/webapp/src/components/tabContentComponents/controlContent.vue index 522f2297..4433208a 100644 --- a/webapp/src/components/tabContentComponents/controlContent.vue +++ b/webapp/src/components/tabContentComponents/controlContent.vue @@ -24,7 +24,7 @@ export default { data() { return { - setStreamId: "control", + setStreamId: this.$options.name, }; }, }; From 22444af143d26f14b57ccd66dd78170e0d6a0984 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Wed, 3 Jun 2026 06:20:29 -0600 Subject: [PATCH 04/10] ui_migration bugfix(component) fix ministream add prop id and visibility stream add and remove --- .../genericComponents/miniStreamDisplay.vue | 61 ++++++++++++++++--- .../labThingsComponents/actionStatusModal.vue | 13 +++- .../stepTemplateWithStream.vue | 9 ++- .../controlComponents/paneControl.vue | 5 ++ .../tabContentComponents/controlContent.vue | 1 + .../settingsComponents/CSMSettings.vue | 9 ++- .../settingsComponents/cameraSettings.vue | 4 +- .../tabContentComponents/slideScanContent.vue | 3 +- .../tabContentComponents/streamContent.vue | 1 - .../tabContentComponents/viewContent.vue | 3 +- 10 files changed, 94 insertions(+), 15 deletions(-) diff --git a/webapp/src/components/genericComponents/miniStreamDisplay.vue b/webapp/src/components/genericComponents/miniStreamDisplay.vue index b699ab97..7d5d3abe 100644 --- a/webapp/src/components/genericComponents/miniStreamDisplay.vue +++ b/webapp/src/components/genericComponents/miniStreamDisplay.vue @@ -5,7 +5,7 @@ class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget" > { + // Always update local visibility instantly this.isVisible = isIntersecting; + + // Clear any previous pending timers to prevent ghost fires + clearTimeout(this.observerTimeout); + + if (isIntersecting) { + // If it's TRUE, we don't want to wait! Turn the stream on immediately. + this.store.addStream(this.streamId); + } else { + // If it's FALSE, wait 50ms before telling the store. + // If a TRUE event fires in the next 50ms (like during a tab switch), + // this timeout will be cancelled and the stream will never flicker off! + this.observerTimeout = setTimeout(() => { + this.store.removeStream(this.streamId); + }, 50); + } }, - { - threshold: 0.0, - }, + { threshold: 0.0 }, ); + this.stopIntersectionObserver = stop; + }, + beforeUnmount() { + clearTimeout(this.observerTimeout); // Clean up the timer! + if (this.stopIntersectionObserver) { + this.stopIntersectionObserver(); + } + this.store.removeStream(this.streamId); }, }; diff --git a/webapp/src/components/labThingsComponents/actionStatusModal.vue b/webapp/src/components/labThingsComponents/actionStatusModal.vue index b73f93de..88ea7f42 100644 --- a/webapp/src/components/labThingsComponents/actionStatusModal.vue +++ b/webapp/src/components/labThingsComponents/actionStatusModal.vue @@ -2,7 +2,11 @@

{{ title }}

- +
@@ -79,6 +83,13 @@ export default { emits: ["terminateTask"], + data() { + return { + // This adds the parent name as value for prop streamId + setStreamId: this.$options.name, + }; + }, + methods: { show() { UIkit.modal(this.$refs.modal).show(); diff --git a/webapp/src/components/modalComponents/calibrationWizardComponents/stepTemplateWithStream.vue b/webapp/src/components/modalComponents/calibrationWizardComponents/stepTemplateWithStream.vue index 4859e838..64344cda 100644 --- a/webapp/src/components/modalComponents/calibrationWizardComponents/stepTemplateWithStream.vue +++ b/webapp/src/components/modalComponents/calibrationWizardComponents/stepTemplateWithStream.vue @@ -1,7 +1,7 @@ @@ -15,6 +15,13 @@ export default { components: { miniStreamDisplay, }, + + data() { + return { + // This adds the parent name as value for prop streamId + setStreamId: this.$options.name, + }; + }, }; diff --git a/webapp/src/components/tabContentComponents/controlComponents/paneControl.vue b/webapp/src/components/tabContentComponents/controlComponents/paneControl.vue index 15c2c84b..b9f0a000 100644 --- a/webapp/src/components/tabContentComponents/controlComponents/paneControl.vue +++ b/webapp/src/components/tabContentComponents/controlComponents/paneControl.vue @@ -34,6 +34,7 @@ import axios from "axios"; import ActionButton from "../../labThingsComponents/actionButton.vue"; import positionControl from "./positionControl.vue"; import autofocusControl from "./autofocusControl.vue"; +import { eventBus } from "../../../eventBus.js"; export default { name: "PaneControl", @@ -61,8 +62,12 @@ export default { this.modalError("No image URI returned from capture task."); return; } + // Emit flash capture stream animation + eventBus.emit("globalFlashStream"); + // To save the returned data, we make a virtual link and click it let imageResponse = await axios.get(imageUri, { responseType: "blob" }); + const url = window.URL.createObjectURL(new Blob([imageResponse.data])); const link = document.createElement("a"); link.href = url; diff --git a/webapp/src/components/tabContentComponents/controlContent.vue b/webapp/src/components/tabContentComponents/controlContent.vue index 4433208a..b69872b8 100644 --- a/webapp/src/components/tabContentComponents/controlContent.vue +++ b/webapp/src/components/tabContentComponents/controlContent.vue @@ -24,6 +24,7 @@ export default { data() { return { + // This adds the parent name as value for prop streamId setStreamId: this.$options.name, }; }, diff --git a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue index 7167d68e..099cd248 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue @@ -10,7 +10,7 @@
- +
@@ -28,6 +28,13 @@ export default { CSMCalibrationSettings, miniStreamDisplay, }, + + data() { + return { + // This adds the parent name as value for prop streamId + setStreamId: this.$options.name, + }; + }, }; diff --git a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue index 3cd28432..80ead801 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/cameraSettings.vue @@ -15,7 +15,7 @@
- +
@@ -41,6 +41,8 @@ export default { data() { return { manualCameraSettings: [], + // This adds the parent name as value for prop streamId + setStreamId: this.$options.name, }; }, diff --git a/webapp/src/components/tabContentComponents/slideScanContent.vue b/webapp/src/components/tabContentComponents/slideScanContent.vue index 6430a69a..6ab945f9 100644 --- a/webapp/src/components/tabContentComponents/slideScanContent.vue +++ b/webapp/src/components/tabContentComponents/slideScanContent.vue @@ -81,7 +81,8 @@ export default { lastStitchedImage: null, scanComplete: false, scan_name: "", - setStreamId: "slideScan", + // This adds the parent name as value for prop streamId + setStreamId: this.$options.name, }; }, diff --git a/webapp/src/components/tabContentComponents/streamContent.vue b/webapp/src/components/tabContentComponents/streamContent.vue index 47d0d0de..7c92e7ca 100644 --- a/webapp/src/components/tabContentComponents/streamContent.vue +++ b/webapp/src/components/tabContentComponents/streamContent.vue @@ -203,7 +203,6 @@ export default { handleDoneResize: function () { // Recalculate size - this.recalculateSize(); // Handle closed stream if (this.displaySize[0] == 0 && this.displaySize[1] == 0) { diff --git a/webapp/src/components/tabContentComponents/viewContent.vue b/webapp/src/components/tabContentComponents/viewContent.vue index 3f346ec8..b9d62989 100644 --- a/webapp/src/components/tabContentComponents/viewContent.vue +++ b/webapp/src/components/tabContentComponents/viewContent.vue @@ -19,7 +19,8 @@ export default { data() { return { - setStreamId: "view", + // This adds the parent name as value for prop streamId + setStreamId: this.$options.name, }; }, }; From 8fb445bcbf93eba0f630d8f06a8e9bf1e41c6562 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Wed, 3 Jun 2026 19:19:03 -0600 Subject: [PATCH 05/10] ui_migration bugfix(component) add debug url and fallback pixel DOM for JPEG_STREAM at beforeUnmount to drop streams --- .../genericComponents/miniStreamDisplay.vue | 15 +++++++++++---- .../components/tabContentComponents/actionTab.vue | 3 ++- .../tabContentComponents/streamContent.vue | 10 ++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/webapp/src/components/genericComponents/miniStreamDisplay.vue b/webapp/src/components/genericComponents/miniStreamDisplay.vue index 7d5d3abe..76f1fd54 100644 --- a/webapp/src/components/genericComponents/miniStreamDisplay.vue +++ b/webapp/src/components/genericComponents/miniStreamDisplay.vue @@ -5,7 +5,7 @@ class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget" > diff --git a/webapp/src/components/tabContentComponents/actionTab.vue b/webapp/src/components/tabContentComponents/actionTab.vue index 7ebcf511..4555c1ee 100644 --- a/webapp/src/components/tabContentComponents/actionTab.vue +++ b/webapp/src/components/tabContentComponents/actionTab.vue @@ -53,7 +53,7 @@ mode.

{{ taskInfoTitle }}

- +
@@ -141,6 +141,7 @@ export default { progress: null, log: [], pollTimer: null, + setStreamId: this.$options.name, }; }, diff --git a/webapp/src/components/tabContentComponents/streamContent.vue b/webapp/src/components/tabContentComponents/streamContent.vue index 7c92e7ca..c267ddf6 100644 --- a/webapp/src/components/tabContentComponents/streamContent.vue +++ b/webapp/src/components/tabContentComponents/streamContent.vue @@ -69,14 +69,16 @@ export default { streamEnabled: function () { return this.ready && !this.disableStream; }, - thisStreamOpen: function () { + streamOpen: function () { // Only a single MJPEG connection should be open at a time return !(this.displaySize[0] == 0) && !(this.displaySize[1] == 0); }, streamImgUri: function () { // Only request the real stream if it's enabled AND currently visible on screen if (this.isVisible && this.streamEnabled) { - return `${this.baseUri}/camera/mjpeg_stream`; + const url = new URL(`${this.baseUri}/camera/mjpeg_stream`); + url.searchParams.append("debugId", this.streamId); + return url.toString(); } // Force the browser to kill the connection by loading a 1 pixel image @@ -141,6 +143,10 @@ export default { this.sizeObserver.disconnect(); this.store.removeStream(this.streamId); } + const imgElement = this.$refs["click-frame"]; + if (imgElement) { + imgElement.src = ONE_PIXEL_FALLBACK; + } }, methods: { From 5e15a423eee71cf59b63bf6be1c9b4047706c4e0 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Wed, 3 Jun 2026 21:43:26 -0600 Subject: [PATCH 06/10] ui_migration bugfix(component) add fallback override for img src --- webapp/src/components/genericComponents/miniStreamDisplay.vue | 4 +++- webapp/src/components/tabContentComponents/streamContent.vue | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/webapp/src/components/genericComponents/miniStreamDisplay.vue b/webapp/src/components/genericComponents/miniStreamDisplay.vue index 76f1fd54..7d8ab644 100644 --- a/webapp/src/components/genericComponents/miniStreamDisplay.vue +++ b/webapp/src/components/genericComponents/miniStreamDisplay.vue @@ -5,7 +5,7 @@ class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget" > Date: Wed, 3 Jun 2026 21:47:49 -0600 Subject: [PATCH 07/10] ui_migration bugfix(component) fix reactivity add markraw on camera and camera_stage_mapping --- webapp/src/components/modalComponents/calibrationWizard.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/modalComponents/calibrationWizard.vue b/webapp/src/components/modalComponents/calibrationWizard.vue index 0dd1dcc2..1e54ec6b 100644 --- a/webapp/src/components/modalComponents/calibrationWizard.vue +++ b/webapp/src/components/modalComponents/calibrationWizard.vue @@ -68,8 +68,8 @@ export default { this.$refs["calibrationModalEl"].addEventListener("hidden", this.onHide); // Check which Things are available on mount. const allCalibrationTasks = { - camera: cameraCalibrationTask, - camera_stage_mapping: cameraStageMappingTask, + camera: markRaw(cameraCalibrationTask), + camera_stage_mapping: markRaw(cameraStageMappingTask), }; this.availableCalibrationTasks = Object.fromEntries( Object.entries(allCalibrationTasks).filter(([thing]) => this.thingAvailable(thing)), From 6f9531059c476e3d99b865829713f259b0a8f366 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Wed, 3 Jun 2026 21:49:22 -0600 Subject: [PATCH 08/10] ui_migration bugfix(component) fix duplicated component name cameraStageMappingTask --- .../calibrationWizardComponents/cameraStageMappingTask.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/modalComponents/calibrationWizardComponents/cameraStageMappingTask.vue b/webapp/src/components/modalComponents/calibrationWizardComponents/cameraStageMappingTask.vue index 48f25e9b..c2a5c8c0 100644 --- a/webapp/src/components/modalComponents/calibrationWizardComponents/cameraStageMappingTask.vue +++ b/webapp/src/components/modalComponents/calibrationWizardComponents/cameraStageMappingTask.vue @@ -18,7 +18,7 @@ import runCsmStep from "./csmSteps/runCsmStep.vue"; import { markRaw } from "vue"; export default { - name: "CameraCalibrationTask", + name: "CameraStageMappingTask", components: { calibrationWizardTask, }, From 70567feb0b8a9cb011504442ad396f6879ca32a1 Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Thu, 4 Jun 2026 11:03:38 +0000 Subject: [PATCH 09/10] Apply suggestions from code review of branch bugfix/streamContent_leaks Co-authored-by: Beth Probert --- webapp/src/components/genericComponents/miniStreamDisplay.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/genericComponents/miniStreamDisplay.vue b/webapp/src/components/genericComponents/miniStreamDisplay.vue index 7d8ab644..2a559d0d 100644 --- a/webapp/src/components/genericComponents/miniStreamDisplay.vue +++ b/webapp/src/components/genericComponents/miniStreamDisplay.vue @@ -50,7 +50,7 @@ export default { return this.ready && !this.disableStream; }, streamImgUri: function () { - // FIX: Require BOTH visibility AND the enabled state + // Require BOTH visibility AND the enabled state if (this.isVisible && this.streamEnabled) { const url = new URL(`${this.baseUri}/camera/mjpeg_stream`); url.searchParams.append("debugId", this.streamId); From 69a7e54ddd4f7034da687a9eab6f924fc0b6fc9b Mon Sep 17 00:00:00 2001 From: Antonio Anaya Date: Thu, 4 Jun 2026 10:52:25 -0600 Subject: [PATCH 10/10] ui_migration chore(suggestions) co-authored by @julianstirling change debugId to streamId on mejpeg_stream URI --- webapp/src/components/genericComponents/miniStreamDisplay.vue | 2 +- webapp/src/components/tabContentComponents/streamContent.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/genericComponents/miniStreamDisplay.vue b/webapp/src/components/genericComponents/miniStreamDisplay.vue index 2a559d0d..8b2a1b18 100644 --- a/webapp/src/components/genericComponents/miniStreamDisplay.vue +++ b/webapp/src/components/genericComponents/miniStreamDisplay.vue @@ -53,7 +53,7 @@ export default { // Require BOTH visibility AND the enabled state if (this.isVisible && this.streamEnabled) { const url = new URL(`${this.baseUri}/camera/mjpeg_stream`); - url.searchParams.append("debugId", this.streamId); + url.searchParams.append("streamId", this.streamId); return url.toString(); } diff --git a/webapp/src/components/tabContentComponents/streamContent.vue b/webapp/src/components/tabContentComponents/streamContent.vue index 425079ad..948f1967 100644 --- a/webapp/src/components/tabContentComponents/streamContent.vue +++ b/webapp/src/components/tabContentComponents/streamContent.vue @@ -73,7 +73,7 @@ export default { // Only request the real stream if it's enabled AND currently visible on screen if (this.isVisible && this.streamEnabled) { const url = new URL(`${this.baseUri}/camera/mjpeg_stream`); - url.searchParams.append("debugId", this.streamId); + url.searchParams.append("streamId", this.streamId); return url.toString(); }