From 337fb5e9c82d39dcca3257fd52c2d85d0189b10b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:18:31 +0000 Subject: [PATCH 01/15] Initial plan From 9528502ea794f81ad53cd4694603e2175531ea00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:26:30 +0000 Subject: [PATCH 02/15] Implement bootstrap-table with AJAX for console view Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/ajax/console.php | 322 ++++++++++++++++++++++++++ web/skins/classic/views/console.php | 288 +++-------------------- web/skins/classic/views/js/console.js | 315 +++++++++++++++++++------ 3 files changed, 609 insertions(+), 316 deletions(-) diff --git a/web/ajax/console.php b/web/ajax/console.php index ff1f82c1b..0dc481a1f 100644 --- a/web/ajax/console.php +++ b/web/ajax/console.php @@ -1,4 +1,23 @@ Username()); + return; + } + $data = queryRequest(); + ajaxResponse($data); + return; + } +} + +// Handle legacy action-based requests if ( canEdit('Monitors') ) { switch ( $_REQUEST['action'] ) { case 'sort' : @@ -30,4 +49,307 @@ if ( canEdit('Monitors') ) { } ajaxError('Unrecognised action '.$_REQUEST['action'].' or insufficient permissions for user ' . $user->Username()); + +// +// FUNCTION DEFINITIONS +// + +function queryRequest() { + global $user, $Servers; + require_once('includes/Monitor.php'); + require_once('includes/Group_Monitor.php'); + + $data = array( + 'total' => 0, + 'totalNotFiltered' => 0, + 'rows' => array() + ); + + // Get pagination parameters + $offset = 0; + if (isset($_REQUEST['offset']) and ($_REQUEST['offset'] != 'NaN')) { + if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) { + ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']); + } else { + $offset = $_REQUEST['offset']; + } + } + + $limit = 0; + if (isset($_REQUEST['limit']) and ($_REQUEST['limit'] != 'NaN')) { + if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) { + ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']); + } else { + $limit = $_REQUEST['limit']; + } + } + + // Get search parameter + $search = isset($_REQUEST['search']) ? $_REQUEST['search'] : ''; + + // Get sort parameters + $sort = isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'Sequence'; + $order = isset($_REQUEST['order']) ? strtoupper($_REQUEST['order']) : 'ASC'; + + // Build monitor query with filters from session + zm_session_start(); + $conditions = array(); + $values = array(); + + // Apply session filters + if (isset($_SESSION['GroupId']) && $_SESSION['GroupId']) { + $GroupIds = is_array($_SESSION['GroupId']) ? $_SESSION['GroupId'] : array($_SESSION['GroupId']); + $conditions[] = 'M.Id IN (SELECT MonitorId FROM Groups_Monitors WHERE GroupId IN (' . implode(',', array_fill(0, count($GroupIds), '?')) . '))'; + $values = array_merge($values, $GroupIds); + } + + foreach (array('ServerId','StorageId') as $filter) { + if (isset($_SESSION[$filter]) && $_SESSION[$filter]) { + $filter_values = is_array($_SESSION[$filter]) ? $_SESSION[$filter] : array($_SESSION[$filter]); + if (count($filter_values)) { + $conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')'; + $values = array_merge($values, $filter_values); + } + } + } + + foreach (array('Capturing','Analysing','Recording') as $filter) { + if (isset($_SESSION[$filter]) && $_SESSION[$filter]) { + $filter_values = is_array($_SESSION[$filter]) ? $_SESSION[$filter] : array($_SESSION[$filter]); + if (count($filter_values)) { + $conditions[] = 'S.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')'; + $values = array_merge($values, $filter_values); + } + } + } + + if (isset($_SESSION['Status']) && $_SESSION['Status']) { + $status_values = is_array($_SESSION['Status']) ? $_SESSION['Status'] : array($_SESSION['Status']); + if (count($status_values)) { + $conditions[] = 'COALESCE(S.Status, IF(M.Type="WebSite","Running","NotRunning")) IN (' . implode(',', array_fill(0, count($status_values), '?')) . ')'; + $values = array_merge($values, $status_values); + } + } + + session_write_close(); + + // Build SQL query + $sql = 'SELECT M.*, S.*, E.* + FROM Monitors AS M + LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id + LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id + WHERE M.`Deleted`=false'; + + if (count($conditions)) { + $sql .= ' AND ' . implode(' AND ', $conditions); + } + + // Get total count before filtering + $monitors = dbFetchAll($sql, null, $values); + $unfiltered_monitors = array(); + foreach ($monitors as $monitor) { + if (visibleMonitor($monitor['Id'])) { + $unfiltered_monitors[] = $monitor; + } + } + $data['totalNotFiltered'] = count($unfiltered_monitors); + + // Apply search filter + $filtered_monitors = $unfiltered_monitors; + if ($search != '') { + $search_lower = strtolower($search); + $filtered_monitors = array_filter($unfiltered_monitors, function($monitor) use ($search_lower) { + $Monitor = new ZM\Monitor($monitor); + return ( + stripos($monitor['Name'], $search_lower) !== false || + stripos($monitor['Function'], $search_lower) !== false || + stripos($Monitor->Source(), $search_lower) !== false || + stripos($monitor['Id'], $search_lower) !== false || + (isset($monitor['Status']) && stripos($monitor['Status'], $search_lower) !== false) + ); + }); + } + + // Apply MonitorName and Source session filters + if (isset($_SESSION['MonitorName']) && $_SESSION['MonitorName']) { + $regexp = $_SESSION['MonitorName']; + if (!strpos($regexp, '/')) $regexp = '/'.$regexp.'/i'; + $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) { + return @preg_match($regexp, $monitor['Name']); + }); + } + + if (isset($_SESSION['Source']) && $_SESSION['Source']) { + $regexp = $_SESSION['Source']; + if (!preg_match("/^\/.+\/[a-z]*$/i", $regexp)) + $regexp = '/'.$regexp.'/i'; + $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) { + $Monitor = new ZM\Monitor($monitor); + return (preg_match($regexp, $Monitor->Source()) || preg_match($regexp, $Monitor->Path())); + }); + } + + $data['total'] = count($filtered_monitors); + + // Sort monitors + usort($filtered_monitors, function($a, $b) use ($sort, $order) { + $aVal = isset($a[$sort]) ? $a[$sort] : ''; + $bVal = isset($b[$sort]) ? $b[$sort] : ''; + + if (is_numeric($aVal) && is_numeric($bVal)) { + $result = $aVal - $bVal; + } else { + $result = strcasecmp($aVal, $bVal); + } + + return $order == 'ASC' ? $result : -$result; + }); + + // Apply pagination + if ($limit > 0) { + $filtered_monitors = array_slice($filtered_monitors, $offset, $limit); + } else { + $filtered_monitors = array_slice($filtered_monitors, $offset); + } + + // Get storage areas and servers + $storage_areas = ZM\Storage::find(); + $StorageById = array(); + foreach ($storage_areas as $S) { + $StorageById[$S->Id()] = $S; + } + + $ServersById = array(); + foreach ($Servers as $s) { + $ServersById[$s->Id()] = $s; + } + + // Get group IDs for each monitor + $monitor_ids = array_map(function($m) { return $m['Id']; }, $filtered_monitors); + $group_ids_by_monitor_id = array(); + if (count($monitor_ids)) { + foreach (ZM\Group_Monitor::find(array('MonitorId'=>$monitor_ids)) as $GM) { + if (!isset($group_ids_by_monitor_id[$GM->MonitorId()])) + $group_ids_by_monitor_id[$GM->MonitorId()] = array(); + $group_ids_by_monitor_id[$GM->MonitorId()][] = $GM->GroupId(); + } + } + + // Process each monitor and build row data + foreach ($filtered_monitors as $monitor) { + $Monitor = new ZM\Monitor($monitor); + $Monitor->GroupIds(isset($group_ids_by_monitor_id[$Monitor->Id()]) ? $group_ids_by_monitor_id[$Monitor->Id()] : array()); + + $row = array(); + $row['Id'] = $monitor['Id']; + $row['Name'] = validHtmlStr($monitor['Name']); + $row['Function'] = $monitor['Function']; + $row['Enabled'] = $monitor['Enabled']; + + // Status + if (!$monitor['Status']) { + if ($monitor['Type'] == 'WebSite') + $monitor['Status'] = 'Running'; + else + $monitor['Status'] = 'NotRunning'; + } + $row['Status'] = $monitor['Status']; + + // Server + if (count($Servers)) { + $Server = isset($ServersById[$monitor['ServerId']]) ? $ServersById[$monitor['ServerId']] : new ZM\Server($monitor['ServerId']); + $row['Server'] = validHtmlStr($Server->Name()); + $row['ServerId'] = $monitor['ServerId']; + } + + // Source + $row['Source'] = validHtmlStr($Monitor->Source()); + $row['Width'] = $Monitor->Width(); + $row['Height'] = $Monitor->Height(); + + // Storage + if (isset($StorageById[$monitor['StorageId']])) { + $row['Storage'] = validHtmlStr($StorageById[$monitor['StorageId']]->Name()); + } else if ($monitor['StorageId']) { + $row['Storage'] = 'Deleted '.$monitor['StorageId'].''; + } else { + $row['Storage'] = ''; + } + + // Event counts + $eventCounts = array('Total', 'Hour', 'Day', 'Week', 'Month', 'Archived'); + foreach ($eventCounts as $period) { + $row[$period.'Events'] = (int)$monitor[$period.'Events']; + $row[$period.'EventDiskSpace'] = human_filesize($monitor[$period.'EventDiskSpace']); + } + + // Zone count + $row['ZoneCount'] = $monitor['ZoneCount']; + + // FPS and bandwidth + $row['CaptureFPS'] = isset($monitor['CaptureFPS']) ? $monitor['CaptureFPS'] : '0.00'; + $row['AnalysisFPS'] = isset($monitor['AnalysisFPS']) ? $monitor['AnalysisFPS'] : '0.00'; + $row['CaptureBandwidth'] = isset($monitor['CaptureBandwidth']) ? $monitor['CaptureBandwidth'] : 0; + $row['Analysing'] = isset($monitor['Analysing']) ? $monitor['Analysing'] : 'None'; + $row['Recording'] = isset($monitor['Recording']) ? $monitor['Recording'] : 'None'; + $row['ONVIF_Event_Listener'] = isset($monitor['ONVIF_Event_Listener']) ? $monitor['ONVIF_Event_Listener'] : 0; + $row['UpdatedOn'] = isset($monitor['UpdatedOn']) ? $monitor['UpdatedOn'] : ''; + $row['Type'] = $monitor['Type']; + $row['Capturing'] = isset($monitor['Capturing']) ? $monitor['Capturing'] : 'None'; + + // Groups + if (canView('Groups')) { + $groups_html = implode('
', + array_map(function($group_id) { + $Group = ZM\Group::find_one(array('Id'=>$group_id)); + if ($Group) { + $Groups = $Group->Parents(); + array_push($Groups, $Group); + } else { + $Groups = array(); + } + return implode(' > ', array_map(function($Group) { + if (canView('Stream')) { + return ''.validHtmlStr($Group->Name()).''; + } else { + return validHtmlStr($Group->Name()); + } + }, $Groups)); + }, $Monitor->GroupIds()) + ); + $row['Groups'] = $groups_html; + } else { + $row['Groups'] = ''; + } + + // Thumbnail + $row['Thumbnail'] = ''; + if (ZM_WEB_LIST_THUMBS && ($monitor['Capturing'] != 'None') && canView('Stream')) { + $options = array(); + $ratio_factor = $Monitor->ViewWidth() ? $Monitor->ViewHeight() / $Monitor->ViewWidth() : 1; + $options['width'] = ZM_WEB_LIST_THUMB_WIDTH; + $options['height'] = ZM_WEB_LIST_THUMB_HEIGHT ? ZM_WEB_LIST_THUMB_HEIGHT : ZM_WEB_LIST_THUMB_WIDTH*$ratio_factor; + $options['scale'] = $Monitor->ViewWidth() ? intval(100*ZM_WEB_LIST_THUMB_WIDTH / $Monitor->ViewWidth()) : 100; + $options['mode'] = 'jpeg'; + $options['frames'] = 1; + + $stillSrc = $Monitor->getStreamSrc($options); + $streamSrc = $Monitor->getStreamSrc(array('scale'=>($options['scale'] > 20 ? 100 : $options['scale']*5))); + + $thmbWidth = ($options['width']) ? 'width:'.$options['width'].'px;' : ''; + $thmbHeight = ($options['height']) ? 'height:'.$options['height'].'px;' : ''; + + $row['Thumbnail'] = '
'. + '
'; + } + + $data['rows'][] = $row; + } + + return $data; +} ?> diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index f1a1c83fc..1fbd05f7e 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -220,277 +220,63 @@ echo $navbar ?>   - +
- +
- + - + - - + + - + - + - + 'and', - 'attr'=>'Monitor', - 'op'=>'IN', - 'val'=>implode(',', $displayMonitorIds) - ) - : ['cnj'=>'and', 'attr'=>'Monitor'] - ); - parseFilter($filter); - echo ''.PHP_EOL; + echo ''.PHP_EOL; } // end foreach eventCounts ?> - + -$displayMonitorIds)) as $GM) { - if ( !isset($group_ids_by_monitor_id[$GM->MonitorId()]) ) - $group_ids_by_monitor_id[$GM->MonitorId()] = array(); - $group_ids_by_monitor_id[$GM->MonitorId()][] = $GM->GroupId(); -} -$monitors = array(); -for ($monitor_i = 0; $monitor_i < count($displayMonitors); $monitor_i += 1) { - $monitor = $displayMonitors[$monitor_i]; - $Monitor = new ZM\Monitor($monitor); - $monitors[] = $Monitor; - $Monitor->GroupIds(isset($group_ids_by_monitor_id[$Monitor->Id()]) ? $group_ids_by_monitor_id[$Monitor->Id()] : array()); - if ( $monitor_i and ( $monitor_i % 200 == 0 ) ) { - echo ''; - echo $table_head; - } # monitor_i % 200 -?> - - - - - - - ' : '>') . $monitor['Id'] ?> -ViewWidth() ? $Monitor->ViewHeight() / $Monitor->ViewWidth() : 1; - $options['width'] = ZM_WEB_LIST_THUMB_WIDTH; - $options['height'] = ZM_WEB_LIST_THUMB_HEIGHT ? ZM_WEB_LIST_THUMB_HEIGHT : ZM_WEB_LIST_THUMB_WIDTH*$ratio_factor; - $options['scale'] = $Monitor->ViewWidth() ? intval(100*ZM_WEB_LIST_THUMB_WIDTH / $Monitor->ViewWidth()) : 100; - $options['mode'] = 'jpeg'; - $options['frames'] = 1; - - $stillSrc = $Monitor->getStreamSrc($options); - $streamSrc = $Monitor->getStreamSrc(array('scale'=>($options['scale'] > 20 ? 100 : $options['scale']*5))); - - $thmbWidth = ( $options['width'] ) ? 'width:'.$options['width'].'px;' : ''; - $thmbHeight = ( $options['height'] ) ? 'height:'.$options['height'].'px;' : ''; - - $imgHTML = '
' : '>'; - $imgHTML .= '
'; - } -?> - - lens - ' : '>') . validHtmlStr($monitor['Name']) ?>
- -
- -', - array_map(function($group_id){ - $Group = ZM\Group::find_one(array('Id'=>$group_id)); - if ( $Group ) { - $Groups = $Group->Parents(); - array_push( $Groups, $Group ); - } - return implode(' > ', array_map(function($Group){ - if (canView('Stream')) { - return ''.validHtmlStr($Group->Name()).''; - } else { - return validHtmlStr($Group->Name()); - } - }, $Groups )); - }, $Monitor->GroupIds())); - } -?> -
- - -'; - } else { - echo translate('Status'.$monitor['Status']).'
'; - if ($monitor['Analysing'] != 'None') { - echo translate('Analysing') . ': '.translate($monitor['Analysing']).'
'; - } - if ($monitor['Recording'] != 'None') { - echo translate('Recording') . ': '.translate($monitor['Recording']) . ($monitor['ONVIF_Event_Listener'] ? ' Use ONVIF' : "") . '
'; - } - ?>
-
-'; - } # end if offline - echo ''.PHP_EOL; - if (count($Servers)) { - $Server = isset($ServersById[$monitor['ServerId']]) ? $ServersById[$monitor['ServerId']] : new ZM\Server($monitor['ServerId']); - echo ''.validHtmlStr($Server->Name()).''.PHP_EOL; - } - echo ''. makeLink( '?view=monitor&mid='.$monitor['Id'], ''.validHtmlStr($Monitor->Source()).'', $Monitor->canEdit()); - echo '
'.$Monitor->Width().'x'.$Monitor->Height(); - echo ''; - if ($show_storage_areas) { - echo ''. - (isset($StorageById[$monitor['StorageId']]) ? validHtmlStr($StorageById[$monitor['StorageId']]->Name()) : ($monitor['StorageId']?'Deleted '.$monitor['StorageId'].'' : '')).''.PHP_EOL; - } - - foreach (array_keys($eventCounts) as $i) { - echo '' : '') . - (int)$monitor[$i.'Events'] . '
' . human_filesize($monitor[$i.'EventDiskSpace']).'
'.PHP_EOL; - } - echo ''. makeLink('?view=zones&mid='.$monitor['Id'], $monitor['ZoneCount'], canView('Monitors')) .''.PHP_EOL; -?> - - - - - - - - - - - - - - - - - -'and', - 'attr'=>'Monitor', - 'op'=>'IN', - 'val'=>implode(',', $displayMonitorIds) - ) - ); - parseFilter($filter); -?> - - ' : '') . - (int)$eventCounts[$i]['totalevents'].'
-
'.human_filesize($eventCounts[$i]['totaldiskspace']) - ?>
- - - -
diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index a1e185519..11c36afe0 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -1,21 +1,164 @@ -function setButtonStates(element) { - const form = element.form; - var checked = 0; - for ( var i=0, len = form.elements.length; i < len; i++ ) { - if ( - form.elements[i].type == "checkbox" && - form.elements[i].name == "markMids[]" - ) { - var tr = $j(form.elements[i]).closest("tr"); - if ( form.elements[i].checked ) { - checked ++; - tr.addClass("danger"); - } else { - tr.removeClass("danger"); +"use strict"; +const table = $j('#consoleTable'); +var ajax = null; +var monitors = {}; // Store monitors by ID for function modal + +// Called by bootstrap-table to retrieve monitor data +function ajaxRequest(params) { + if (ajax) ajax.abort(); + ajax = $j.ajax({ + method: 'POST', + url: thisUrl + '?view=request&request=console&task=query', + data: params.data, + timeout: 0, + success: function(data) { + if (data.result == 'Error') { + alert(data.message); + return; + } + var rows = processRows(data.rows); + // Store monitors for function modal + rows.forEach(function(row) { + monitors[row.Id] = row; + }); + // rearrange the result into what bootstrap-table expects + params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows}); + }, + error: function(jqXHR) { + if (jqXHR.statusText != 'abort') { + console.log("error", jqXHR); } } - } - if ( checked ) { + }); +} + +function processRows(rows) { + $j.each(rows, function(ndx, row) { + var mid = row.Id; + var stream_available = canView.Stream && (row.Type == 'WebSite' || (row.CaptureFPS && row.Capturing != 'None')); + + // Determine status classes + var source_class = 'infoText'; + var source_class_reason = ''; + var fps_report_seconds = 60 + 30; // Simplified calculation + + if ((!row.Status || row.Status == 'NotRunning') && row.Type != 'WebSite') { + source_class = 'errorText'; + source_class_reason = 'Not Running'; + } else if (!row.UpdatedOn || (new Date(row.UpdatedOn).getTime() < Date.now() - fps_report_seconds * 1000)) { + source_class = 'errorText'; + source_class_reason = 'Offline'; + } else { + if (row.CaptureFPS == '0.00') { + source_class = 'errorText'; + source_class_reason = 'No capture FPS'; + } else if (!row.AnalysisFPS && row.Analysing != 'None') { + source_class = 'warnText'; + source_class_reason = 'No analysis FPS'; + } + } + + var dot_class = source_class; + var dot_class_reason = source_class_reason; + + // Format Id column + if (stream_available) { + row.Id = '' + mid + ''; + } else { + row.Id = mid; + } + + // Format Name column with status indicator, link, thumbnail, and groups + var nameHtml = 'lens '; + if (stream_available) { + nameHtml += '' + row.Name + ''; + } else { + nameHtml += row.Name; + } + nameHtml += '
'; + + // Add thumbnail if available + if (row.Thumbnail) { + nameHtml += row.Thumbnail; + } + + // Add groups + if (row.Groups) { + nameHtml += '
' + row.Groups + '
'; + } + + row.Name = nameHtml; + + // Format Function column with status and FPS info + var functionHtml = ''; + if (!row.UpdatedOn || (new Date(row.UpdatedOn).getTime() < Date.now() - fps_report_seconds * 1000)) { + functionHtml = 'Offline
'; + } else { + functionHtml = 'Status: ' + row.Status + '
'; + if (row.Analysing && row.Analysing != 'None') { + functionHtml += 'Analysing: ' + row.Analysing + '
'; + } + if (row.Recording && row.Recording != 'None') { + functionHtml += 'Recording: ' + row.Recording; + if (row.ONVIF_Event_Listener) { + functionHtml += ' Use ONVIF'; + } + functionHtml += '
'; + } + functionHtml += '
'; + + var fps_string = ''; + if (row.CaptureFPS) { + fps_string = row.CaptureFPS; + } + if (row.AnalysisFPS && row.Analysing != 'None') { + fps_string += '/' + row.AnalysisFPS; + } + if (fps_string) fps_string += ' fps'; + if (row.CaptureBandwidth) { + fps_string += ' ' + row.CaptureBandwidth; + } + functionHtml += fps_string + '
'; + } + row.Function = functionHtml; + + // Format Source column with link and dimensions + var sourceHtml = ''; + if (canEdit.Monitors) { + sourceHtml = '' + row.Source + ''; + } else { + sourceHtml = '' + row.Source + ''; + } + sourceHtml += '
' + row.Width + 'x' + row.Height; + row.Source = sourceHtml; + + // Format event count columns + var eventPeriods = ['Total', 'Hour', 'Day', 'Week', 'Month', 'Archived']; + eventPeriods.forEach(function(period) { + if (canView.Events) { + row[period + 'Events'] = '' + + row[period + 'Events'] + '
' + + row[period + 'EventDiskSpace'] + '
'; + } else { + row[period + 'Events'] = row[period + 'Events'] + '
' + + row[period + 'EventDiskSpace'] + '
'; + } + }); + + // Format Zones column + if (canView.Monitors) { + row.ZoneCount = '' + row.ZoneCount + ''; + } + }); + + return rows; +} + +function setButtonStates(element) { + const selections = table.bootstrapTable('getSelections'); + const form = element ? element.form : document.forms['monitorForm']; + + if (selections && selections.length > 0) { form.editBtn.disabled = false; form.deleteBtn.disabled = false; form.selectBtn.disabled = false; @@ -44,73 +187,87 @@ function cloneMonitor(element) { alert('Need create monitors privilege'); return; } - var form = element.form; - var monitorId = -1; - // get the value of the first checkbox - for ( var i=0, len=form.elements.length; i < len; i++ ) { - if ( - form.elements[i].type == "checkbox" && - form.elements[i].name == "markMids[]" && - form.elements[i].checked - ) { - monitorId = form.elements[i].value; - break; + const selections = table.bootstrapTable('getSelections'); + if (selections.length > 0) { + // Get the actual monitor ID from the first selection + var monitorId = selections[0].Id; + // Remove HTML if present + var match = monitorId.match(/mid=(\d+)/); + if (match) { + monitorId = match[1]; } - } // end foreach element - if ( monitorId != -1 ) { - window.location.assign('?view=monitor&dupId='+monitorId); + window.location.assign('?view=monitor&dupId=' + monitorId); } else { alert('Please select a monitor to clone'); } } -function editMonitor( element ) { - var form = element.form; - var monitorIds = Array(); - - for ( var i = 0; i < form.elements.length; i++ ) { - if ( - form.elements[i].type == "checkbox" && - form.elements[i].name == "markMids[]" && - form.elements[i].checked - ) { - monitorIds.push( form.elements[i].value ); - } - } // end foreach checkboxes - if ( monitorIds.length == 1 ) { - window.location.assign('?view=monitor&mid='+monitorIds[0]); - } else if ( monitorIds.length > 1 ) { - window.location.assign( '?view=monitors&'+(monitorIds.map(function(mid) { - return 'mids[]='+mid; +function editMonitor(element) { + const selections = table.bootstrapTable('getSelections'); + if (selections.length == 0) return; + + var monitorIds = selections.map(function(sel) { + var mid = sel.Id; + // Extract numeric ID from HTML if present + var match = mid.toString().match(/mid=(\d+)/); + return match ? match[1] : mid; + }); + + if (monitorIds.length == 1) { + window.location.assign('?view=monitor&mid=' + monitorIds[0]); + } else if (monitorIds.length > 1) { + window.location.assign('?view=monitors&' + (monitorIds.map(function(mid) { + return 'mids[]=' + mid; }).join('&'))); } } -function deleteMonitor( element ) { +function deleteMonitor(element) { if (confirm('Deleting a monitor only marks it as deleted. Events will age out. If you want them to be immediately removed, please delete them first.\nAre you sure you wish to delete?')) { const form = element.form; form.elements['action'].value = 'delete'; + + // Get selected monitor IDs and add them to the form + const selections = table.bootstrapTable('getSelections'); + selections.forEach(function(sel) { + var mid = sel.Id; + // Extract numeric ID from HTML if present + var match = mid.toString().match(/mid=(\d+)/); + mid = match ? match[1] : mid; + + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'markMids[]'; + input.value = mid; + form.appendChild(input); + }); + form.submit(); } } function selectMonitor(element) { - var form = element.form; - var url = thisUrl+'?view=console'; - for ( var i = 0; i < form.elements.length; i++ ) { - if ( - form.elements[i].type == 'checkbox' && - form.elements[i].name == 'markMids[]' && - form.elements[i].checked - ) { - url += '&MonitorId[]='+form.elements[i].value; - } - } + const selections = table.bootstrapTable('getSelections'); + var url = thisUrl + '?view=console'; + + selections.forEach(function(sel) { + var mid = sel.Id; + // Extract numeric ID from HTML if present + var match = mid.toString().match(/mid=(\d+)/); + mid = match ? match[1] : mid; + url += '&MonitorId[]=' + mid; + }); + window.location.replace(url); } function reloadWindow() { - window.location.replace( thisUrl ); + // Use table refresh instead of full page reload + if (table && table.length) { + table.bootstrapTable('refresh'); + } else { + window.location.replace(thisUrl); + } } // Manage the the Function modal and its buttons @@ -182,9 +339,34 @@ function manageFunctionModal(evt) { } // end function manageFunctionModal function initPage() { + // Init the bootstrap-table + table.bootstrapTable({icons: icons}); + + // Enable or disable buttons based on current selection and user rights + table.on('check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table', + function() { + const selections = table.bootstrapTable('getSelections'); + const form = document.forms['monitorForm']; + + if (selections.length > 0) { + form.editBtn.disabled = false; + form.deleteBtn.disabled = false; + form.selectBtn.disabled = false; + form.cloneBtn.disabled = false; + } else { + form.editBtn.disabled = true; + form.deleteBtn.disabled = true; + form.selectBtn.disabled = true; + form.cloneBtn.disabled = true; + } + } + ); + + // Setup automatic refresh with table refresh instead of page reload if (consoleRefreshTimeout > 0) { setInterval(reloadWindow, consoleRefreshTimeout); } + if ( showDonatePopup ) { $j.getJSON(thisUrl + '?request=modal&modal=donate') .done(function(data) { @@ -199,13 +381,16 @@ function initPage() { .fail(logAjaxFail); } - - // Setup the thumbnail video animation - if (!isMobile()) initThumbAnimation(); + // Setup the thumbnail video animation after table loads + table.on('post-body.bs.table', function() { + if (!isMobile()) initThumbAnimation(); + $j('.functionLnk').click(manageFunctionModal); + }); $j('.functionLnk').click(manageFunctionModal); - // Makes table sortable + // Makes table sortable - disabled by default, enabled by Sort button + // Note: This may need adjustment for bootstrap-table compatibility $j('#consoleTableBody').sortable({ disabled: true, update: applySort, From f02d74c4ed31e4e3b3f43b7d953ac85228c8f964 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:29:21 +0000 Subject: [PATCH 03/15] Fix session handling and monitor ID extraction in bootstrap-table implementation Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/ajax/console.php | 43 +++++++++++++++-------- web/skins/classic/views/js/console.js | 34 ++++++------------ web/skins/classic/views/js/console.js.php | 32 ++++++++--------- 3 files changed, 52 insertions(+), 57 deletions(-) diff --git a/web/ajax/console.php b/web/ajax/console.php index 0dc481a1f..a35b1e471 100644 --- a/web/ajax/console.php +++ b/web/ajax/console.php @@ -96,16 +96,31 @@ function queryRequest() { $conditions = array(); $values = array(); - // Apply session filters - if (isset($_SESSION['GroupId']) && $_SESSION['GroupId']) { - $GroupIds = is_array($_SESSION['GroupId']) ? $_SESSION['GroupId'] : array($_SESSION['GroupId']); + // Store session filters for later use + $session_filters = array( + 'GroupId' => isset($_SESSION['GroupId']) ? $_SESSION['GroupId'] : null, + 'ServerId' => isset($_SESSION['ServerId']) ? $_SESSION['ServerId'] : null, + 'StorageId' => isset($_SESSION['StorageId']) ? $_SESSION['StorageId'] : null, + 'Capturing' => isset($_SESSION['Capturing']) ? $_SESSION['Capturing'] : null, + 'Analysing' => isset($_SESSION['Analysing']) ? $_SESSION['Analysing'] : null, + 'Recording' => isset($_SESSION['Recording']) ? $_SESSION['Recording'] : null, + 'Status' => isset($_SESSION['Status']) ? $_SESSION['Status'] : null, + 'MonitorName' => isset($_SESSION['MonitorName']) ? $_SESSION['MonitorName'] : null, + 'Source' => isset($_SESSION['Source']) ? $_SESSION['Source'] : null + ); + + session_write_close(); + + // Apply session filters to SQL + if ($session_filters['GroupId']) { + $GroupIds = is_array($session_filters['GroupId']) ? $session_filters['GroupId'] : array($session_filters['GroupId']); $conditions[] = 'M.Id IN (SELECT MonitorId FROM Groups_Monitors WHERE GroupId IN (' . implode(',', array_fill(0, count($GroupIds), '?')) . '))'; $values = array_merge($values, $GroupIds); } foreach (array('ServerId','StorageId') as $filter) { - if (isset($_SESSION[$filter]) && $_SESSION[$filter]) { - $filter_values = is_array($_SESSION[$filter]) ? $_SESSION[$filter] : array($_SESSION[$filter]); + if ($session_filters[$filter]) { + $filter_values = is_array($session_filters[$filter]) ? $session_filters[$filter] : array($session_filters[$filter]); if (count($filter_values)) { $conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')'; $values = array_merge($values, $filter_values); @@ -114,8 +129,8 @@ function queryRequest() { } foreach (array('Capturing','Analysing','Recording') as $filter) { - if (isset($_SESSION[$filter]) && $_SESSION[$filter]) { - $filter_values = is_array($_SESSION[$filter]) ? $_SESSION[$filter] : array($_SESSION[$filter]); + if ($session_filters[$filter]) { + $filter_values = is_array($session_filters[$filter]) ? $session_filters[$filter] : array($session_filters[$filter]); if (count($filter_values)) { $conditions[] = 'S.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')'; $values = array_merge($values, $filter_values); @@ -123,16 +138,14 @@ function queryRequest() { } } - if (isset($_SESSION['Status']) && $_SESSION['Status']) { - $status_values = is_array($_SESSION['Status']) ? $_SESSION['Status'] : array($_SESSION['Status']); + if ($session_filters['Status']) { + $status_values = is_array($session_filters['Status']) ? $session_filters['Status'] : array($session_filters['Status']); if (count($status_values)) { $conditions[] = 'COALESCE(S.Status, IF(M.Type="WebSite","Running","NotRunning")) IN (' . implode(',', array_fill(0, count($status_values), '?')) . ')'; $values = array_merge($values, $status_values); } } - session_write_close(); - // Build SQL query $sql = 'SELECT M.*, S.*, E.* FROM Monitors AS M @@ -171,16 +184,16 @@ function queryRequest() { } // Apply MonitorName and Source session filters - if (isset($_SESSION['MonitorName']) && $_SESSION['MonitorName']) { - $regexp = $_SESSION['MonitorName']; + if ($session_filters['MonitorName']) { + $regexp = $session_filters['MonitorName']; if (!strpos($regexp, '/')) $regexp = '/'.$regexp.'/i'; $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) { return @preg_match($regexp, $monitor['Name']); }); } - if (isset($_SESSION['Source']) && $_SESSION['Source']) { - $regexp = $_SESSION['Source']; + if ($session_filters['Source']) { + $regexp = $session_filters['Source']; if (!preg_match("/^\/.+\/[a-z]*$/i", $regexp)) $regexp = '/'.$regexp.'/i'; $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) { diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index 11c36afe0..2e287ea82 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -17,9 +17,9 @@ function ajaxRequest(params) { return; } var rows = processRows(data.rows); - // Store monitors for function modal + // Store monitors for function modal using original ID rows.forEach(function(row) { - monitors[row.Id] = row; + monitors[row._id] = row; }); // rearrange the result into what bootstrap-table expects params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows}); @@ -35,6 +35,10 @@ function ajaxRequest(params) { function processRows(rows) { $j.each(rows, function(ndx, row) { var mid = row.Id; + + // Store original ID for later use + row._id = mid; + var stream_available = canView.Stream && (row.Type == 'WebSite' || (row.CaptureFPS && row.Capturing != 'None')); // Determine status classes @@ -189,13 +193,7 @@ function cloneMonitor(element) { } const selections = table.bootstrapTable('getSelections'); if (selections.length > 0) { - // Get the actual monitor ID from the first selection - var monitorId = selections[0].Id; - // Remove HTML if present - var match = monitorId.match(/mid=(\d+)/); - if (match) { - monitorId = match[1]; - } + var monitorId = selections[0]._id; window.location.assign('?view=monitor&dupId=' + monitorId); } else { alert('Please select a monitor to clone'); @@ -207,10 +205,7 @@ function editMonitor(element) { if (selections.length == 0) return; var monitorIds = selections.map(function(sel) { - var mid = sel.Id; - // Extract numeric ID from HTML if present - var match = mid.toString().match(/mid=(\d+)/); - return match ? match[1] : mid; + return sel._id; }); if (monitorIds.length == 1) { @@ -230,15 +225,10 @@ function deleteMonitor(element) { // Get selected monitor IDs and add them to the form const selections = table.bootstrapTable('getSelections'); selections.forEach(function(sel) { - var mid = sel.Id; - // Extract numeric ID from HTML if present - var match = mid.toString().match(/mid=(\d+)/); - mid = match ? match[1] : mid; - var input = document.createElement('input'); input.type = 'hidden'; input.name = 'markMids[]'; - input.value = mid; + input.value = sel._id; form.appendChild(input); }); @@ -251,11 +241,7 @@ function selectMonitor(element) { var url = thisUrl + '?view=console'; selections.forEach(function(sel) { - var mid = sel.Id; - // Extract numeric ID from HTML if present - var match = mid.toString().match(/mid=(\d+)/); - mid = match ? match[1] : mid; - url += '&MonitorId[]=' + mid; + url += '&MonitorId[]=' + sel._id; }); window.location.replace(url); diff --git a/web/skins/classic/views/js/console.js.php b/web/skins/classic/views/js/console.js.php index 736d1611f..9b200ad41 100644 --- a/web/skins/classic/views/js/console.js.php +++ b/web/skins/classic/views/js/console.js.php @@ -11,22 +11,18 @@ if ( canEdit('System') && ZM_DYN_SHOW_DONATE_REMINDER ) { } ?> var showDonatePopup = ; -var monitors = new Array(); - - monitors[Id() ?>] = { - 'Id': Id() ?>, - 'Name': 'Name() ?>', - 'ViewWidth': ViewWidth() ?>, - 'ViewHeight':ViewHeight() ?>, - 'Url': 'UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>', - 'Type': 'Type() ?>', - 'Function': 'Function() ?>', - 'Enabled': 'Enabled() ?>', - 'DecodingEnabled': 'DecodingEnabled() ?>' + +// Variables for bootstrap-table +var canView = { + 'Stream': , + 'Events': , + 'Monitors': }; - + +var canEdit = { + 'Monitors': , + 'Events': +}; + +var ZM_WEB_EVENTS_VIEW = ''; + From 81bcdb456736bdf6cbe0c4e8eb1e034265abb339 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:31:52 +0000 Subject: [PATCH 04/15] Address code review feedback: improve efficiency and fix event bindings Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/ajax/console.php | 10 ++++++---- web/skins/classic/views/js/console.js | 9 ++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/web/ajax/console.php b/web/ajax/console.php index a35b1e471..f37b98a22 100644 --- a/web/ajax/console.php +++ b/web/ajax/console.php @@ -172,11 +172,13 @@ function queryRequest() { if ($search != '') { $search_lower = strtolower($search); $filtered_monitors = array_filter($unfiltered_monitors, function($monitor) use ($search_lower) { - $Monitor = new ZM\Monitor($monitor); + // Search across common fields without creating Monitor object return ( stripos($monitor['Name'], $search_lower) !== false || stripos($monitor['Function'], $search_lower) !== false || - stripos($Monitor->Source(), $search_lower) !== false || + stripos($monitor['Path'], $search_lower) !== false || + stripos($monitor['Device'], $search_lower) !== false || + stripos($monitor['Host'], $search_lower) !== false || stripos($monitor['Id'], $search_lower) !== false || (isset($monitor['Status']) && stripos($monitor['Status'], $search_lower) !== false) ); @@ -197,8 +199,8 @@ function queryRequest() { if (!preg_match("/^\/.+\/[a-z]*$/i", $regexp)) $regexp = '/'.$regexp.'/i'; $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) { - $Monitor = new ZM\Monitor($monitor); - return (preg_match($regexp, $Monitor->Source()) || preg_match($regexp, $Monitor->Path())); + // Match against Path field directly instead of creating Monitor object + return (preg_match($regexp, $monitor['Path']) || preg_match($regexp, $monitor['Device']) || preg_match($regexp, $monitor['Host'])); }); } diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index 2e287ea82..596f80d0b 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -44,7 +44,8 @@ function processRows(rows) { // Determine status classes var source_class = 'infoText'; var source_class_reason = ''; - var fps_report_seconds = 60 + 30; // Simplified calculation + // FPS report interval: 60 seconds base + 30 seconds buffer for FPSReportInterval + var fps_report_seconds = 90; if ((!row.Status || row.Status == 'NotRunning') && row.Type != 'WebSite') { source_class = 'errorText'; @@ -158,9 +159,9 @@ function processRows(rows) { return rows; } -function setButtonStates(element) { +function setButtonStates() { const selections = table.bootstrapTable('getSelections'); - const form = element ? element.form : document.forms['monitorForm']; + const form = document.forms['monitorForm']; if (selections && selections.length > 0) { form.editBtn.disabled = false; @@ -373,8 +374,6 @@ function initPage() { $j('.functionLnk').click(manageFunctionModal); }); - $j('.functionLnk').click(manageFunctionModal); - // Makes table sortable - disabled by default, enabled by Sort button // Note: This may need adjustment for bootstrap-table compatibility $j('#consoleTableBody').sortable({ From c81fa1e6627d67f65c147fab241f74574e441827 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:40:47 +0000 Subject: [PATCH 05/15] Address feedback: remove conflicting variables, add table.show(), separate thumbnail column, add event totals in header/footer Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/views/console.php | 67 ++++++++++++++++++++++- web/skins/classic/views/js/console.js | 19 ++++--- web/skins/classic/views/js/console.js.php | 13 +---- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index 1fbd05f7e..c725f8c8a 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -256,6 +256,9 @@ echo $navbar ?> + + + videocam  @@ -268,7 +271,23 @@ echo $navbar ?> '.$eventCounts[$i]['title'].''.PHP_EOL; + $filter = addFilterTerm( + $eventCounts[$i]['filter'], + count($eventCounts[$i]['filter']['Query']['terms']), + count($displayMonitorIds) != $colAllAvailableMonitors #Add monitors to the filter only if the filter limit is set + ? array( + 'cnj'=>'and', + 'attr'=>'Monitor', + 'op'=>'IN', + 'val'=>implode(',', $displayMonitorIds) + ) + : ['cnj'=>'and', 'attr'=>'Monitor'] + ); + parseFilter($filter); + echo '' : '') + .$eventCounts[$i]['title'] + .''.PHP_EOL; } // end foreach eventCounts ?> @@ -277,6 +296,52 @@ echo $navbar ?> + + + + + + + + + + + + + + + + + + +'and', + 'attr'=>'Monitor', + 'op'=>'IN', + 'val'=>implode(',', $displayMonitorIds) + ) + ); + parseFilter($filter); +?> + + ' : '') . + (int)$eventCounts[$i]['totalevents'].'
+
'.human_filesize($eventCounts[$i]['totaldiskspace']) + ?>
+ + + +
diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index 596f80d0b..fee14524b 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -73,23 +73,20 @@ function processRows(rows) { row.Id = mid; } - // Format Name column with status indicator, link, thumbnail, and groups + // Thumbnail goes in its own column if enabled + // (row.Thumbnail is already set from AJAX response) + + // Format Name column with status indicator, link, and groups (no thumbnail) var nameHtml = 'lens '; if (stream_available) { nameHtml += '' + row.Name + ''; } else { nameHtml += row.Name; } - nameHtml += '
'; - - // Add thumbnail if available - if (row.Thumbnail) { - nameHtml += row.Thumbnail; - } // Add groups if (row.Groups) { - nameHtml += '
' + row.Groups + '
'; + nameHtml += '
' + row.Groups + '
'; } row.Name = nameHtml; @@ -326,6 +323,9 @@ function manageFunctionModal(evt) { } // end function manageFunctionModal function initPage() { + // Remove the thumbnail column from the DOM if thumbnails are off globally + if (!ZM_WEB_LIST_THUMBS) $j('th[data-field="Thumbnail"]').remove(); + // Init the bootstrap-table table.bootstrapTable({icons: icons}); @@ -380,6 +380,9 @@ function initPage() { disabled: true, update: applySort, axis: 'Y'} ); + + // Make the table visible after initialization + table.show(); } // end function initPage function sortMonitors(button) { diff --git a/web/skins/classic/views/js/console.js.php b/web/skins/classic/views/js/console.js.php index 9b200ad41..cb7bcb6fe 100644 --- a/web/skins/classic/views/js/console.js.php +++ b/web/skins/classic/views/js/console.js.php @@ -12,17 +12,6 @@ if ( canEdit('System') && ZM_DYN_SHOW_DONATE_REMINDER ) { ?> var showDonatePopup = ; -// Variables for bootstrap-table -var canView = { - 'Stream': , - 'Events': , - 'Monitors': -}; - -var canEdit = { - 'Monitors': , - 'Events': -}; - var ZM_WEB_EVENTS_VIEW = ''; +var ZM_WEB_LIST_THUMBS = ; From a3d0d8d19b019232789b8d84fc8f34ce596ee202 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:42:24 +0000 Subject: [PATCH 06/15] Fix PHP code style: use consistent array syntax and proper comment style Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/views/console.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index c725f8c8a..4ee921241 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -274,14 +274,14 @@ echo $navbar ?> $filter = addFilterTerm( $eventCounts[$i]['filter'], count($eventCounts[$i]['filter']['Query']['terms']), - count($displayMonitorIds) != $colAllAvailableMonitors #Add monitors to the filter only if the filter limit is set + count($displayMonitorIds) != $colAllAvailableMonitors // Add monitors to the filter only if the filter limit is set ? array( 'cnj'=>'and', 'attr'=>'Monitor', 'op'=>'IN', 'val'=>implode(',', $displayMonitorIds) ) - : ['cnj'=>'and', 'attr'=>'Monitor'] + : array('cnj'=>'and', 'attr'=>'Monitor') ); parseFilter($filter); echo ' Date: Tue, 6 Jan 2026 22:56:05 +0000 Subject: [PATCH 07/15] Fix filtering and remove unnecessary ZM_WEB_LIST_THUMBS variable Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/ajax/console.php | 2 +- web/skins/classic/views/js/console.js | 3 --- web/skins/classic/views/js/console.js.php | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/web/ajax/console.php b/web/ajax/console.php index f37b98a22..1e44ed36a 100644 --- a/web/ajax/console.php +++ b/web/ajax/console.php @@ -132,7 +132,7 @@ function queryRequest() { if ($session_filters[$filter]) { $filter_values = is_array($session_filters[$filter]) ? $session_filters[$filter] : array($session_filters[$filter]); if (count($filter_values)) { - $conditions[] = 'S.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')'; + $conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')'; $values = array_merge($values, $filter_values); } } diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index fee14524b..953ceb3cf 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -323,9 +323,6 @@ function manageFunctionModal(evt) { } // end function manageFunctionModal function initPage() { - // Remove the thumbnail column from the DOM if thumbnails are off globally - if (!ZM_WEB_LIST_THUMBS) $j('th[data-field="Thumbnail"]').remove(); - // Init the bootstrap-table table.bootstrapTable({icons: icons}); diff --git a/web/skins/classic/views/js/console.js.php b/web/skins/classic/views/js/console.js.php index cb7bcb6fe..424ae4372 100644 --- a/web/skins/classic/views/js/console.js.php +++ b/web/skins/classic/views/js/console.js.php @@ -13,5 +13,4 @@ if ( canEdit('System') && ZM_DYN_SHOW_DONATE_REMINDER ) { var showDonatePopup = ; var ZM_WEB_EVENTS_VIEW = ''; -var ZM_WEB_LIST_THUMBS = ; From 185651b659aaa2037db7c642e55b68d583b72fc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 00:33:03 +0000 Subject: [PATCH 08/15] Add dynamic footer totals generation via AJAX Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/ajax/console.php | 42 +++++++++++++++++++++++++++ web/skins/classic/views/js/console.js | 42 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/web/ajax/console.php b/web/ajax/console.php index 1e44ed36a..61be3aa29 100644 --- a/web/ajax/console.php +++ b/web/ajax/console.php @@ -251,10 +251,37 @@ function queryRequest() { } // Process each monitor and build row data + $footer_totals = array( + 'monitor_count' => count($filtered_monitors), + 'total_bandwidth' => 0, + 'total_fps' => 0, + 'total_analysis_fps' => 0, + 'total_zones' => 0, + 'event_totals' => array( + 'Total' => array('events' => 0, 'diskspace' => 0), + 'Hour' => array('events' => 0, 'diskspace' => 0), + 'Day' => array('events' => 0, 'diskspace' => 0), + 'Week' => array('events' => 0, 'diskspace' => 0), + 'Month' => array('events' => 0, 'diskspace' => 0), + 'Archived' => array('events' => 0, 'diskspace' => 0) + ) + ); + foreach ($filtered_monitors as $monitor) { $Monitor = new ZM\Monitor($monitor); $Monitor->GroupIds(isset($group_ids_by_monitor_id[$Monitor->Id()]) ? $group_ids_by_monitor_id[$Monitor->Id()] : array()); + // Accumulate footer totals + $footer_totals['total_bandwidth'] += isset($monitor['CaptureBandwidth']) ? $monitor['CaptureBandwidth'] : 0; + $footer_totals['total_fps'] += isset($monitor['CaptureFPS']) ? floatval($monitor['CaptureFPS']) : 0; + $footer_totals['total_analysis_fps'] += isset($monitor['AnalysisFPS']) ? floatval($monitor['AnalysisFPS']) : 0; + $footer_totals['total_zones'] += isset($monitor['ZoneCount']) ? intval($monitor['ZoneCount']) : 0; + + foreach (array('Total', 'Hour', 'Day', 'Week', 'Month', 'Archived') as $period) { + $footer_totals['event_totals'][$period]['events'] += isset($monitor[$period.'Events']) ? intval($monitor[$period.'Events']) : 0; + $footer_totals['event_totals'][$period]['diskspace'] += isset($monitor[$period.'EventDiskSpace']) ? intval($monitor[$period.'EventDiskSpace']) : 0; + } + $row = array(); $row['Id'] = $monitor['Id']; $row['Name'] = validHtmlStr($monitor['Name']); @@ -365,6 +392,21 @@ function queryRequest() { $data['rows'][] = $row; } + // Add footer totals to response + $data['footer'] = array( + 'monitor_count' => $footer_totals['monitor_count'], + 'bandwidth_fps' => human_filesize($footer_totals['total_bandwidth']).'/s '. + round($footer_totals['total_fps'], 2).' fps / '. + round($footer_totals['total_analysis_fps'], 2).' fps', + 'total_zones' => $footer_totals['total_zones'] + ); + + // Add formatted event totals to footer + foreach (array('Total', 'Hour', 'Day', 'Week', 'Month', 'Archived') as $period) { + $data['footer'][$period.'Events'] = $footer_totals['event_totals'][$period]['events']; + $data['footer'][$period.'EventDiskSpace'] = human_filesize($footer_totals['event_totals'][$period]['diskspace']); + } + return $data; } ?> diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index 953ceb3cf..c89765019 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -3,6 +3,42 @@ const table = $j('#consoleTable'); var ajax = null; var monitors = {}; // Store monitors by ID for function modal +// Update footer with dynamic totals +function updateFooter(footer) { + // Update monitor count (in Id column if shown) + if ($j('tfoot .colId').length) { + $j('tfoot .colId').html('Total: ' + footer.monitor_count); + } + + // Update bandwidth/FPS (in Function column) + if ($j('tfoot .colFunction').length) { + $j('tfoot .colFunction').html(footer.bandwidth_fps); + } + + // Update event totals + var eventPeriods = ['Total', 'Hour', 'Day', 'Week', 'Month', 'Archived']; + eventPeriods.forEach(function(period) { + var cell = $j('tfoot td.colEvents:eq(' + eventPeriods.indexOf(period) + ')'); + if (cell.length) { + var link = cell.find('a'); + if (link.length) { + // Preserve the link but update the count + var newHtml = footer[period + 'Events'] + '
' + + footer[period + 'EventDiskSpace'] + '
'; + link.html(newHtml); + } else { + cell.html(footer[period + 'Events'] + '
' + + footer[period + 'EventDiskSpace'] + '
'); + } + } + }); + + // Update zone count + if ($j('tfoot .colZones').length) { + $j('tfoot .colZones').text(footer.total_zones); + } +} + // Called by bootstrap-table to retrieve monitor data function ajaxRequest(params) { if (ajax) ajax.abort(); @@ -21,6 +57,12 @@ function ajaxRequest(params) { rows.forEach(function(row) { monitors[row._id] = row; }); + + // Update footer with totals from response + if (data.footer) { + updateFooter(data.footer); + } + // rearrange the result into what bootstrap-table expects params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows}); }, From 3a4c464a03357a49c3835eb893eae784ede5a593 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 00:54:44 +0000 Subject: [PATCH 09/15] Fix footer visibility and improve footer update selectors Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/views/console.php | 1 + web/skins/classic/views/js/console.js | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index 4ee921241..20ae252f7 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -238,6 +238,7 @@ echo $navbar ?> data-remember-order="true" data-show-columns="true" data-show-export="true" + data-show-footer="true" data-toolbar="#toolbar" data-sort-name="Sequence" data-sort-order="asc" diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index c89765019..e94e75fc8 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -5,21 +5,21 @@ var monitors = {}; // Store monitors by ID for function modal // Update footer with dynamic totals function updateFooter(footer) { + // Target the footer within the bootstrap-table wrapper + var footerRow = $j('#consoleTable tfoot tr'); + // Update monitor count (in Id column if shown) - if ($j('tfoot .colId').length) { - $j('tfoot .colId').html('Total: ' + footer.monitor_count); - } + footerRow.find('td.colId').html('Total: ' + footer.monitor_count); // Update bandwidth/FPS (in Function column) - if ($j('tfoot .colFunction').length) { - $j('tfoot .colFunction').html(footer.bandwidth_fps); - } + footerRow.find('td.colFunction').html(footer.bandwidth_fps); // Update event totals var eventPeriods = ['Total', 'Hour', 'Day', 'Week', 'Month', 'Archived']; - eventPeriods.forEach(function(period) { - var cell = $j('tfoot td.colEvents:eq(' + eventPeriods.indexOf(period) + ')'); - if (cell.length) { + var eventCells = footerRow.find('td.colEvents'); + eventPeriods.forEach(function(period, index) { + if (eventCells.length > index) { + var cell = $j(eventCells[index]); var link = cell.find('a'); if (link.length) { // Preserve the link but update the count @@ -34,9 +34,7 @@ function updateFooter(footer) { }); // Update zone count - if ($j('tfoot .colZones').length) { - $j('tfoot .colZones').text(footer.total_zones); - } + footerRow.find('td.colZones').text(footer.total_zones); } // Called by bootstrap-table to retrieve monitor data From 91defbbada5f875a7e7a33a605b1de8ee8f8058e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:11:09 +0000 Subject: [PATCH 10/15] Fix footer selectors to handle bootstrap-table th elements and inner divs Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/views/js/console.js | 37 +++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index e94e75fc8..edb931f01 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -6,35 +6,56 @@ var monitors = {}; // Store monitors by ID for function modal // Update footer with dynamic totals function updateFooter(footer) { // Target the footer within the bootstrap-table wrapper - var footerRow = $j('#consoleTable tfoot tr'); + // Bootstrap-table may transform td to th and wrap content in divs + var footerRow = $j('#consoleTable').closest('.bootstrap-table').find('tfoot tr'); + if (!footerRow.length) { + footerRow = $j('#consoleTable tfoot tr'); + } + + // Helper function to update cell content (handles th-inner and fht-cell divs) + function updateCell(selector, content) { + var cell = footerRow.find(selector); + if (cell.length) { + // Check if bootstrap-table has wrapped content in divs + var innerDiv = cell.find('.th-inner, .fht-cell'); + if (innerDiv.length) { + innerDiv.html(content); + } else { + cell.html(content); + } + } + } // Update monitor count (in Id column if shown) - footerRow.find('td.colId').html('Total: ' + footer.monitor_count); + updateCell('td.colId, th.colId', 'Total: ' + footer.monitor_count); // Update bandwidth/FPS (in Function column) - footerRow.find('td.colFunction').html(footer.bandwidth_fps); + updateCell('td.colFunction, th.colFunction', footer.bandwidth_fps); // Update event totals var eventPeriods = ['Total', 'Hour', 'Day', 'Week', 'Month', 'Archived']; - var eventCells = footerRow.find('td.colEvents'); + var eventCells = footerRow.find('td.colEvents, th.colEvents'); eventPeriods.forEach(function(period, index) { if (eventCells.length > index) { var cell = $j(eventCells[index]); - var link = cell.find('a'); + var innerDiv = cell.find('.th-inner, .fht-cell'); + var target = innerDiv.length ? innerDiv : cell; + + var link = target.find('a'); if (link.length) { // Preserve the link but update the count var newHtml = footer[period + 'Events'] + '
' + footer[period + 'EventDiskSpace'] + '
'; link.html(newHtml); } else { - cell.html(footer[period + 'Events'] + '
' + - footer[period + 'EventDiskSpace'] + '
'); + target.html(footer[period + 'Events'] + '
' + + footer[period + 'EventDiskSpace'] + '
'); } } }); // Update zone count - footerRow.find('td.colZones').text(footer.total_zones); + updateCell('td.colZones, th.colZones', footer.total_zones); } // Called by bootstrap-table to retrieve monitor data From 6104e15bcbc48b54a99fb46e0fa1fac0f336cf97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:33:06 +0000 Subject: [PATCH 11/15] Move updateFooter call after params.success and target only th-inner div Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/views/js/console.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index edb931f01..970f9961b 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -12,12 +12,12 @@ function updateFooter(footer) { footerRow = $j('#consoleTable tfoot tr'); } - // Helper function to update cell content (handles th-inner and fht-cell divs) + // Helper function to update cell content (only updates th-inner div) function updateCell(selector, content) { var cell = footerRow.find(selector); if (cell.length) { - // Check if bootstrap-table has wrapped content in divs - var innerDiv = cell.find('.th-inner, .fht-cell'); + // Only update the th-inner div if it exists + var innerDiv = cell.find('.th-inner'); if (innerDiv.length) { innerDiv.html(content); } else { @@ -38,7 +38,8 @@ function updateFooter(footer) { eventPeriods.forEach(function(period, index) { if (eventCells.length > index) { var cell = $j(eventCells[index]); - var innerDiv = cell.find('.th-inner, .fht-cell'); + // Only update the th-inner div if it exists + var innerDiv = cell.find('.th-inner'); var target = innerDiv.length ? innerDiv : cell; var link = target.find('a'); @@ -77,13 +78,13 @@ function ajaxRequest(params) { monitors[row._id] = row; }); - // Update footer with totals from response + // rearrange the result into what bootstrap-table expects + params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows}); + + // Update footer with totals from response after table is rendered if (data.footer) { updateFooter(data.footer); } - - // rearrange the result into what bootstrap-table expects - params.success({total: data.total, totalNotFiltered: data.totalNotFiltered, rows: rows}); }, error: function(jqXHR) { if (jqXHR.statusText != 'abort') { From f6598a023f4757a4fa93ff3bbc013ecf79b123cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:47:21 +0000 Subject: [PATCH 12/15] Add monitorFilterOnChange function for AJAX filter updates on console view Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/views/_monitor_filters.php | 19 +++++++++++++------ web/skins/classic/views/js/console.js | 11 +++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/web/skins/classic/views/_monitor_filters.php b/web/skins/classic/views/_monitor_filters.php index 6fbd58416..f7bfaad1e 100644 --- a/web/skins/classic/views/_monitor_filters.php +++ b/web/skins/classic/views/_monitor_filters.php @@ -19,12 +19,16 @@ // function addFilterSelect($name, $options) { + global $view; + // Use monitorFilterOnChange on console view for AJAX refresh, submitThisForm elsewhere + $onChangeFunction = ($view == 'console') ? 'monitorFilterOnChange' : 'submitThisForm'; + $html = ''; $html .= ''; $html .= htmlSelect($name.'[]', $options, (isset($_SESSION[$name])?$_SESSION[$name]:''), array( - 'data-on-change'=>'submitThisForm', + 'data-on-change'=>$onChangeFunction, 'class'=>'chosen', 'multiple'=>'multiple', 'data-placeholder'=>'All', @@ -52,8 +56,11 @@ function addButtonResetForFilterSelect($nameSelect) { } function buildMonitorsFilters() { - global $user, $Servers; + global $user, $Servers, $view; require_once('includes/Monitor.php'); + + // Use monitorFilterOnChange on console view for AJAX refresh, submitThisForm elsewhere + $onChangeFunction = ($view == 'console') ? 'monitorFilterOnChange' : 'submitThisForm'; zm_session_start(); foreach (array('GroupId','Capturing','Analysing','Recording','ServerId','StorageId','Status','MonitorId','MonitorName','Source') as $var) { @@ -151,7 +158,7 @@ function buildMonitorsFilters() { $html .= htmlSelect('ServerId[]', $ServersById, (isset($_SESSION['ServerId'])?$_SESSION['ServerId']:''), array( - 'data-on-change'=>'submitThisForm', + 'data-on-change'=>$onChangeFunction, 'class'=>'chosen', 'multiple'=>'multiple', 'data-placeholder'=>'All', @@ -168,7 +175,7 @@ function buildMonitorsFilters() { $html .= htmlSelect('StorageId[]', $StorageById, (isset($_SESSION['StorageId'])?$_SESSION['StorageId']:''), array( - 'data-on-change'=>'submitThisForm', + 'data-on-change'=>$onChangeFunction, 'class'=>'chosen', 'multiple'=>'multiple', 'data-placeholder'=>'All', @@ -189,7 +196,7 @@ function buildMonitorsFilters() { $html .= htmlSelect( 'Status[]', $status_options, ( isset($_SESSION['Status']) ? $_SESSION['Status'] : '' ), array( - 'data-on-change'=>'submitThisForm', + 'data-on-change'=>$onChangeFunction, 'class'=>'chosen', 'multiple'=>'multiple', 'data-placeholder'=>'All' @@ -296,7 +303,7 @@ function buildMonitorsFilters() { $html .= ''; $html .= htmlSelect('MonitorId[]', $monitors_dropdown, $selected_monitor_ids, array( - 'data-on-change'=>'submitThisForm', + 'data-on-change'=>$onChangeFunction, 'class'=>'chosen', 'multiple'=>'multiple', 'data-placeholder'=>'All', diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index 970f9961b..c29d74133 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -384,6 +384,17 @@ function manageFunctionModal(evt) { $j('#modalFunction').modal('show'); } // end function manageFunctionModal +// Called when monitor filters change - refreshes table via AJAX instead of full page reload +function monitorFilterOnChange() { + // On console view with bootstrap-table, just refresh the table + if (typeof table !== 'undefined' && table.length) { + table.bootstrapTable('refresh'); + } else { + // Fall back to full page reload on other views + submitThisForm(); + } +} + function initPage() { // Init the bootstrap-table table.bootstrapTable({icons: icons}); From e3ed9247a6ffc3d66eddeaa641cd3a2e2c310aef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:56:03 +0000 Subject: [PATCH 13/15] Remove redundant status line from Function column Merged commit 448b2b3 from SteveGilvarry/ZoneMinder. The Function column was showing "Status: Connected" which was redundant with the colored dot indicator in the Name column. Removed the status line while keeping "Offline" text for monitors that haven't reported in 90+ seconds. This reduces visual clutter and makes the Function column focus on actual function-related information. Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/views/js/console.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index c29d74133..3a524941b 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -158,7 +158,7 @@ function processRows(rows) { if (!row.UpdatedOn || (new Date(row.UpdatedOn).getTime() < Date.now() - fps_report_seconds * 1000)) { functionHtml = 'Offline
'; } else { - functionHtml = 'Status: ' + row.Status + '
'; + functionHtml = ''; if (row.Analysing && row.Analysing != 'None') { functionHtml += 'Analysing: ' + row.Analysing + '
'; } From 67dff2f77478b9c65054d486b9690111f7e4f776 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:05:53 +0000 Subject: [PATCH 14/15] Enable thumbnail hover expansion in console view Merged commit 04a3b60 from SteveGilvarry/ZoneMinder. Thumbnails were getting cut off by table boundaries when hovering. Added relative positioning to .colThumbnail parent and absolute positioning with high z-index to hover state, allowing thumbnails to expand beyond table boundaries and overlay other content. Works with existing .zoom-console class in skin.js for animation. Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/css/base/views/console.css | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/web/skins/classic/css/base/views/console.css b/web/skins/classic/css/base/views/console.css index 09e2e85a9..bcc09ce8b 100644 --- a/web/skins/classic/css/base/views/console.css +++ b/web/skins/classic/css/base/views/console.css @@ -161,8 +161,20 @@ body.sticky #monitorList thead { } } -.colThumbnail img:hover { - /* - position: absolute; - */ +.colThumbnail { + position: relative; +} + +.colThumbnail img:hover { + position: absolute; + z-index: 1000; +} + +/* Allow thumbnails to expand beyond table boundaries */ +.consoleTable:has(.colThumbnail img:hover) { + overflow: visible !important; +} + +#monitorList:has(.colThumbnail img:hover) { + overflow: visible !important; } From 02983f5a9991ee54d70cefe4aaf2cc36cc627d09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:23:15 +0000 Subject: [PATCH 15/15] Reduce Id column width in console view Merged commit fd671d7 from SteveGilvarry/ZoneMinder. The Id column was too wide for displaying numeric monitor IDs. Added width constraint of 70px with centered text alignment to make better use of horizontal space. Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- web/skins/classic/css/base/views/console.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/skins/classic/css/base/views/console.css b/web/skins/classic/css/base/views/console.css index bcc09ce8b..d6c91e66f 100644 --- a/web/skins/classic/css/base/views/console.css +++ b/web/skins/classic/css/base/views/console.css @@ -90,6 +90,11 @@ text-align: center; } +.consoleTable .colId { + width: 70px; + text-align: center; +} + .consoleTable .colLeftButtons { text-align: left; }