diff --git a/web/ajax/console.php b/web/ajax/console.php index ff1f82c1b..61be3aa29 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,364 @@ 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(); + + // 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 ($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); + } + } + } + + foreach (array('Capturing','Analysing','Recording') as $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); + } + } + } + + 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); + } + } + + // 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) { + // Search across common fields without creating Monitor object + return ( + stripos($monitor['Name'], $search_lower) !== false || + stripos($monitor['Function'], $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) + ); + }); + } + + // Apply MonitorName and Source session filters + 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 ($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) { + // 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'])); + }); + } + + $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 + $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']); + $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; + } + + // 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/css/base/views/console.css b/web/skins/classic/css/base/views/console.css index 09e2e85a9..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; } @@ -161,8 +166,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; } 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/console.php b/web/skins/classic/views/console.php index f1a1c83fc..20ae252f7 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -220,232 +220,81 @@ echo $navbar ?>   - +
- +
- + - + - - + + + + + - + - + - + 'and', 'attr'=>'Monitor', 'op'=>'IN', 'val'=>implode(',', $displayMonitorIds) ) - : ['cnj'=>'and', 'attr'=>'Monitor'] + : array('cnj'=>'and', 'attr'=>'Monitor') ); parseFilter($filter); - 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; -?> - - @@ -454,6 +303,9 @@ for ($monitor_i = 0; $monitor_i < count($displayMonitors); $monitor_i += 1) { + + + index) { + var cell = $j(eventCells[index]); + // 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'); + if (link.length) { + // Preserve the link but update the count + var newHtml = footer[period + 'Events'] + '
' + + footer[period + 'EventDiskSpace'] + '
'; + link.html(newHtml); + } else { + target.html(footer[period + 'Events'] + '
' + + footer[period + 'EventDiskSpace'] + '
'); + } + } + }); + + // Update zone count + updateCell('td.colZones, th.colZones', footer.total_zones); +} + +// 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 using original ID + 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}); + + // Update footer with totals from response after table is rendered + if (data.footer) { + updateFooter(data.footer); + } + }, + error: function(jqXHR) { + if (jqXHR.statusText != 'abort') { + console.log("error", jqXHR); + } + } + }); +} + +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 + var source_class = 'infoText'; + var source_class_reason = ''; + // 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'; + 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; + } + + // 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; + } + + // 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 = ''; + 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() { + const selections = table.bootstrapTable('getSelections'); + const form = document.forms['monitorForm']; + + if (selections && selections.length > 0) { form.editBtn.disabled = false; form.deleteBtn.disabled = false; form.selectBtn.disabled = false; @@ -44,73 +251,69 @@ 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; - } - } // end foreach element - if ( monitorId != -1 ) { - window.location.assign('?view=monitor&dupId='+monitorId); + const selections = table.bootstrapTable('getSelections'); + if (selections.length > 0) { + var monitorId = selections[0]._id; + 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) { + return sel._id; + }); + + 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 input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'markMids[]'; + input.value = sel._id; + 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) { + url += '&MonitorId[]=' + sel._id; + }); + 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 @@ -181,10 +384,46 @@ 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}); + + // 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,17 +438,21 @@ function initPage() { .fail(logAjaxFail); } + // Setup the thumbnail video animation after table loads + table.on('post-body.bs.table', function() { + if (!isMobile()) initThumbAnimation(); + $j('.functionLnk').click(manageFunctionModal); + }); - // Setup the thumbnail video animation - if (!isMobile()) initThumbAnimation(); - - $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, 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 736d1611f..424ae4372 100644 --- a/web/skins/classic/views/js/console.js.php +++ b/web/skins/classic/views/js/console.js.php @@ -11,22 +11,6 @@ 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() ?>' -}; - + +var ZM_WEB_EVENTS_VIEW = ''; +