From 0d7abadb61ab5ecbc090f52fe6ea2c8cecacd2c9 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sun, 9 Aug 2026 21:35:39 -0400 Subject: [PATCH] fix: make the API DateTime filter an overlap test DateTime is a pseudo-attribute meaning "the event was running then", so a window over it should select events that overlap the window. The code applied the term's own operator to both StartDateTime and EndDateTime, which turns the upper bound into StartDateTime <= max AND EndDateTime <= max -- a containment test. Any event spanning the end of the window was dropped, which under continuous recording is most of them, and with events longer than the window, all of them. Montage review showed an empty timeline as a result. The lower bound now tests the event's end and the upper bound its start. The EndDateTime IS NULL allowance is also bounded. It was there so an event still being written stays visible, but as written it made every crash-orphaned event ever recorded match every window: montage review was returning events from two weeks earlier and nothing from the requested hour. An event with no EndDateTime still has Length, flushed every few seconds by zmc, so StartDateTime + Length is its effective end; only an event with neither falls back to NOW(). This is the same expression the Event model already uses for its EndTimeSecs virtual field. Verified against a live instance: for a 09:43-10:43 window that had one overlapping event per monitor, the API returned 8 events for monitor 1, all crash orphans from 2026-07-26 and none from the window. It now returns the overlapping event, and montage review draws it and renders its frames. --- web/api/app/Controller/EventsController.php | 64 ++++++++++++--------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/web/api/app/Controller/EventsController.php b/web/api/app/Controller/EventsController.php index 3798acb27..bf03274e6 100644 --- a/web/api/app/Controller/EventsController.php +++ b/web/api/app/Controller/EventsController.php @@ -82,37 +82,47 @@ class EventsController extends AppController { } $conditions = $this->FilterComponent->buildFilter($named_params); #ZM\Debug(print_r($conditions, true)); + # DateTime is a pseudo-attribute meaning "the event was running then", so a + # window over it is an overlap test: the event started by the upper bound + # and had not finished by the lower bound. Applying the term's own operator + # to both StartDateTime and EndDateTime instead makes the upper bound a + # containment test, which drops every event that spans the end of the + # window -- with continuous recording that is most of them. + $datetime_terms = array(); foreach ($conditions as $k=>$v) { if ( 0 === strpos($k, 'DateTime') ) { - $new_start = preg_replace('/DateTime/', 'StartDateTime', $k); - $new_end = preg_replace('/DateTime/', 'EndDateTime', $k); - if (isset($conditions['OR'])) { - $conditions['AND'] = [ - ['OR' => $conditions['OR']], - [ - [$new_start => $conditions[$k]], - ['OR'=>[ - $new_end => $conditions[$k], - 'EndDateTime IS NULL', - ] - ] - ] - ]; - unset($conditions['OR']); - } else { - $conditions['OR'] = [ - [$new_start => $conditions[$k]], - [ - 'OR'=>[ - $new_end => $conditions[$k], - 'EndDateTime IS NULL', - ] - ] - ]; - } + $datetime_terms[$k] = $v; unset($conditions[$k]); } - } // end foreach condition + } + if ($datetime_terms) { + # An event still being written has no EndDateTime, but it is not + # unbounded: zmc flushes Length every few seconds, so StartDateTime + + # Length is its effective end. Only an event with neither falls back to + # NOW(). Treating a missing EndDateTime as "matches any window" instead + # made every crash-orphaned event ever recorded match every query. + $effective_end = '(CASE' + .' WHEN Event.EndDateTime IS NOT NULL THEN Event.EndDateTime' + .' WHEN Event.Length > 0 THEN DATE_ADD(Event.StartDateTime, INTERVAL FLOOR(Event.Length) SECOND)' + .' ELSE NOW() END)'; + $ds = $this->Event->getDataSource(); + foreach ($datetime_terms as $k=>$v) { + $op = trim(substr($k, strlen('DateTime'))); + if ($op == '<' or $op == '<=') { + # Upper bound: the event must have started by then. + $conditions[] = array('Event.StartDateTime '.$op => $v); + } else if ($op == '>' or $op == '>=') { + # Lower bound: the event must not have ended before then. + $conditions[] = $effective_end.' '.$op.' '.$ds->value($v, 'string'); + } else { + # Anything else (=, !=) still matches against either end. + $conditions[] = array('OR' => array( + array('Event.StartDateTime '.$op => $v), + array('Event.EndDateTime '.$op => $v), + )); + } + } + } // end if datetime terms #ZM\Debug(print_r($conditions, true)); } else {