From 0b53412f8f6dc9aaa892b674afcdf0b0c485f301 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 20 Oct 2026 08:06:24 -0400 Subject: [PATCH] fix: don't constrain prev/next events by StartDateTime when sorting by another field getNearEvents() built the prev/next query from the full (sort value, Id) tuple, which already orders events strictly in the chosen sort, but then also added AND E.StartDateTime >= / <= the current event's time. That extra predicate is only consistent when the sort field is StartDateTime. When events are sorted by another field -- MonitorName to group cameras, or Id -- it dropped events that are correctly next in that order but started earlier or later than the current one: the next monitor's first event (which often started before the current monitor's last), or a concurrently recorded event with a higher Id. Gapless playback then skipped over the events of every monitor but the first. A fresh, single-timeline database rarely shows this because Id order tracks time order; a database with several monitors recording in parallel, or one migrated from another system, makes the two diverge and exposes it. Drop the StartDateTime constraint and let the tuple be the whole ordering. It cannot reintroduce the earlier playback loop because (sort value, Id) is a strict total order. refs #4186 Co-Authored-By: Claude Opus 4.8 --- web/ajax/status.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/ajax/status.php b/web/ajax/status.php index 0f2fbf462b..3472e7b73e 100644 --- a/web/ajax/status.php +++ b/web/ajax/status.php @@ -557,11 +557,17 @@ function getNearEvents() { # e.g. several monitors starting events in the same second, so Prev/Next must compare the full # (sort value, Id) tuple. Comparing the sort value alone with >= / <= lets the event after a tie # pick the one before it as its Next, and playback loops between them forever. + # + # That tuple is the whole ordering: do not also constrain E.StartDateTime. When the sort is not + # StartDateTime (e.g. MonitorName, grouping cameras, or Id) a time constraint drops events that + # are correctly next in the chosen order but started earlier or later than the current one -- the + # next monitor's first event, or a concurrently recorded event with a higher Id, was skipped + # (refs #4186). $after = ($sortOrder == 'ASC') ? '>' : '<'; $before = ($sortOrder == 'ASC') ? '<' : '>'; $reverseOrder = ($sortOrder == 'ASC') ? 'DESC' : 'ASC'; - $nearEvent = function($cmp, $idCmp, $startCmp, $order, $idOrder) use ($eventId, $event, $filter, $sortColumn, $sortValue) { + $nearEvent = function($cmp, $idCmp, $order, $idOrder) use ($eventId, $filter, $sortColumn, $sortValue) { $sql = ' SELECT E.Id AS Id, E.StartDateTime AS StartDateTime FROM Events AS E @@ -579,8 +585,6 @@ function getNearEvents() { if ($filter->sql()) { $sql .= ' AND ('.$filter->sql().')'; } - $sql .= ' AND E.StartDateTime '.$startCmp.' ?'; - $values[] = $event['StartDateTime']; $sql .= ' ORDER BY '.$sortColumn.' '.$order; if ($sortColumn != 'E.Id') { $sql .= ', E.Id '.$idOrder; @@ -594,8 +598,8 @@ function getNearEvents() { return dbFetchNext($result); }; - $prevEvent = $nearEvent($before, '<', '<=', $reverseOrder, 'DESC'); - $nextEvent = $nearEvent($after, '>', '>=', $sortOrder, 'ASC'); + $prevEvent = $nearEvent($before, '<', $reverseOrder, 'DESC'); + $nextEvent = $nearEvent($after, '>', $sortOrder, 'ASC'); if ( $prevEvent ) { $NearEvents['PrevEventId'] = $prevEvent['Id'];