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 ?>
-
+
-
+ ';
- 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 = '';
- }
-?>
-
- 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 = ''.translate($name).' ';
$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;
}