From c6c4d818be313136c43fd12425e5535e33d71a41 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 8 Apr 2026 07:36:17 -0500 Subject: [PATCH] motion search fixes - tweak progress bar to exclude heatmap and inactive segments - show metrics immediately on search start - fix preview frame loading race - fix polygon missing after dialog remount - don't try to drag the image when dragging vertex of polygon --- .../motion-search/MotionSearchDialog.tsx | 46 +++++++++++++---- .../motion-search/MotionSearchROICanvas.tsx | 49 ++++++++++++++----- .../views/motion-search/MotionSearchView.tsx | 27 +++++----- 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/web/src/views/motion-search/MotionSearchDialog.tsx b/web/src/views/motion-search/MotionSearchDialog.tsx index cff49b4b1..ee8652b5e 100644 --- a/web/src/views/motion-search/MotionSearchDialog.tsx +++ b/web/src/views/motion-search/MotionSearchDialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { isDesktop, isIOS, isMobile } from "react-device-detect"; import { FaArrowRight, FaCalendarAlt, FaCheckCircle } from "react-icons/fa"; @@ -42,7 +42,6 @@ import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { TimezoneAwareCalendar } from "@/components/overlay/ReviewActivityCalendar"; import { useApiHost } from "@/api"; -import { useResizeObserver } from "@/hooks/resize-observer"; import { useFormattedTimestamp, use24HourTime } from "@/hooks/use-date-utils"; import { getUTCOffset } from "@/utils/dateUtil"; import useSWR from "swr"; @@ -113,11 +112,35 @@ export default function MotionSearchDialog({ }: MotionSearchDialogProps) { const { t } = useTranslation(["views/motionSearch", "common"]); const apiHost = useApiHost(); - const containerRef = useRef(null); - const [{ width: containerWidth, height: containerHeight }] = - useResizeObserver(containerRef); + const [containerNode, setContainerNode] = useState( + null, + ); + const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); + const containerWidth = containerSize.width; + const containerHeight = containerSize.height; const [imageLoaded, setImageLoaded] = useState(false); + useEffect(() => { + if (!containerNode) { + return; + } + + const measure = () => { + const rect = containerNode.getBoundingClientRect(); + setContainerSize((prev) => + prev.width === rect.width && prev.height === rect.height + ? prev + : { width: rect.width, height: rect.height }, + ); + }; + + measure(); + + const observer = new ResizeObserver(() => measure()); + observer.observe(containerNode); + return () => observer.disconnect(); + }, [containerNode]); + const cameraConfig = useMemo(() => { if (!selectedCamera) return undefined; return config.cameras[selectedCamera]; @@ -258,16 +281,16 @@ export default function MotionSearchDialog({ }} >
- {selectedCamera && cameraConfig && imageSize.width > 0 ? ( + {selectedCamera && cameraConfig ? (
setImageLoaded(true)} + ref={(node) => { + if (node?.complete && node.naturalWidth > 0) { + setImageLoaded(true); + } + }} /> {!imageLoaded && (
diff --git a/web/src/views/motion-search/MotionSearchROICanvas.tsx b/web/src/views/motion-search/MotionSearchROICanvas.tsx index 43fac7a48..b0d90da81 100644 --- a/web/src/views/motion-search/MotionSearchROICanvas.tsx +++ b/web/src/views/motion-search/MotionSearchROICanvas.tsx @@ -1,10 +1,9 @@ -import { useCallback, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Stage, Layer, Line, Circle, Image } from "react-konva"; import Konva from "konva"; import type { KonvaEventObject } from "konva/lib/Node"; import { flattenPoints } from "@/utils/canvasUtil"; import { cn } from "@/lib/utils"; -import { useResizeObserver } from "@/hooks/resize-observer"; type MotionSearchROICanvasProps = { camera: string; @@ -30,18 +29,40 @@ export default function MotionSearchROICanvas({ motionHeatmap, showMotionHeatmap = false, }: MotionSearchROICanvasProps) { - const containerRef = useRef(null); const stageRef = useRef(null); - const [{ width: containerWidth, height: containerHeight }] = - useResizeObserver(containerRef); - - const stageSize = useMemo( - () => ({ - width: containerWidth > 0 ? Math.ceil(containerWidth) : 0, - height: containerHeight > 0 ? Math.ceil(containerHeight) : 0, - }), - [containerHeight, containerWidth], + const [containerNode, setContainerNode] = useState( + null, ); + const [stageSize, setStageSize] = useState({ width: 0, height: 0 }); + + useEffect(() => { + if (!containerNode) { + return; + } + + const apply = (width: number, height: number) => { + setStageSize((prev) => { + const next = { + width: width > 0 ? Math.ceil(width) : 0, + height: height > 0 ? Math.ceil(height) : 0, + }; + if (prev.width === next.width && prev.height === next.height) { + return prev; + } + return next; + }); + }; + + apply(containerNode.clientWidth, containerNode.clientHeight); + + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (!entry) return; + apply(entry.contentRect.width, entry.contentRect.height); + }); + observer.observe(containerNode); + return () => observer.disconnect(); + }, [containerNode]); const videoRect = useMemo(() => { const stageWidth = stageSize.width; @@ -317,7 +338,7 @@ export default function MotionSearchROICanvas({ return (
e.evt.stopPropagation()} + onTouchStart={(e) => e.evt.stopPropagation()} onDragMove={(e) => handlePointDragMove(e, index)} onMouseOver={(e) => handleMouseOverPoint(e, index)} onMouseOut={(e) => handleMouseOutPoint(e, index)} diff --git a/web/src/views/motion-search/MotionSearchView.tsx b/web/src/views/motion-search/MotionSearchView.tsx index 961c1065c..34c52cc5d 100644 --- a/web/src/views/motion-search/MotionSearchView.tsx +++ b/web/src/views/motion-search/MotionSearchView.tsx @@ -994,15 +994,20 @@ export default function MotionSearchView({ ); const progressMetrics = jobStatus?.metrics ?? searchMetrics; - const progressValue = - progressMetrics && progressMetrics.segments_scanned > 0 - ? Math.min( - 100, - (progressMetrics.segments_processed / - progressMetrics.segments_scanned) * - 100, - ) - : 0; + const progressValue = (() => { + if (!progressMetrics || progressMetrics.segments_scanned <= 0) { + return 0; + } + const skipped = + progressMetrics.heatmap_roi_skip_segments + + progressMetrics.metadata_inactive_segments; + const totalWork = progressMetrics.segments_scanned - skipped; + const doneWork = progressMetrics.segments_processed - skipped; + if (totalWork <= 0) { + return 100; + } + return Math.min(100, Math.max(0, (doneWork / totalWork) * 100)); + })(); const resultsPanel = ( <> @@ -1036,8 +1041,8 @@ export default function MotionSearchView({
)} - {searchMetrics && searchResults.length > 0 && ( -
+ {searchMetrics && (isSearching || searchResults.length > 0) && ( +
{t("metrics.segmentsScanned")}