Merge remote-tracking branch 'upstream/copilot/refactor-console-view-table'

This commit is contained in:
Isaac Connor
2026-01-07 09:58:20 -05:00
6 changed files with 771 additions and 289 deletions

View File

@@ -1,4 +1,23 @@
<?php
$message = '';
$data = array();
// Handle query task for bootstrap-table AJAX requests
if (!empty($_REQUEST['task'])) {
$task = $_REQUEST['task'];
if ($task == 'query') {
if (!canView('Monitors')) {
ajaxError('Insufficient permissions for user '.$user->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'] = '<span class="error">Deleted '.$monitor['StorageId'].'</span>';
} 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('<br/>',
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(' &gt; ', array_map(function($Group) {
if (canView('Stream')) {
return '<a href="?view=montagereview&amp;GroupId='.$Group->Id().'">'.validHtmlStr($Group->Name()).'</a>';
} 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'] = '<div class="colThumbnail" style="'.$thmbHeight.'"><a href="?view=watch&amp;mid='.$monitor['Id'].'">'.
'<img id="thumbnail'.$Monitor->Id().'" src="'.$stillSrc.'" style="'.$thmbWidth.$thmbHeight.
'" stream_src="'.$streamSrc.'" still_src="'.$stillSrc.'"'.
($options['width'] ? ' width="'.$options['width'].'"' : '').
($options['height'] ? ' height="'.$options['height'].'"' : '').
' loading="lazy" /></a></div>';
}
$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;
}
?>

View File

@@ -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;
}

View File

@@ -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 = '<span class="term '.$name.'Filter"><label>'.translate($name).'</label>';
$html .= '<span class="term-value-wrapper">';
$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 .= '<span class="term-value-wrapper">';
$html .= htmlSelect('MonitorId[]', $monitors_dropdown, $selected_monitor_ids,
array(
'data-on-change'=>'submitThisForm',
'data-on-change'=>$onChangeFunction,
'class'=>'chosen',
'multiple'=>'multiple',
'data-placeholder'=>'All',

View File

@@ -220,232 +220,81 @@ echo $navbar ?>
&nbsp;<a href="#" data-flip-control-object="#fbpanel"><i id="fbflip" class="material-icons" data-icon-visible="filter_alt_off" data-icon-hidden="filter_alt"></i></a>
</div><!-- contentButtons -->
<?php
ob_start();
?>
<div id="monitorList" class="container-fluid table-responsive-sm">
<table class="table table-striped table-hover table-condensed consoleTable">
<table
id="consoleTable"
data-locale="<?php echo i18n() ?>"
data-side-pagination="server"
data-ajax="ajaxRequest"
data-pagination="true"
data-page-size="<?php echo ZM_WEB_EVENTS_PER_PAGE ?>"
data-page-list="[10, 25, 50, 100, 200, All]"
data-search="true"
data-cookie="true"
data-cookie-same-site="Strict"
data-cookie-id-table="zmConsoleTable"
data-cookie-expire="2y"
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"
data-show-refresh="true"
data-click-to-select="true"
data-maintain-meta-data="true"
data-buttons-class="btn btn-normal"
data-mobile-responsive="true"
class="table table-striped table-hover table-condensed consoleTable"
style="display:none;"
>
<thead class="thead-highlight">
<tr>
<?php if ($canEditMonitors) { ?>
<th class="colMark"><input type="checkbox" name="toggleCheck" value="1" data-checkbox-name="markMids[]" data-on-click-this="updateFormCheckboxesByName"/></th>
<th data-sortable="false" data-field="toggleCheck" data-checkbox="true"></th>
<?php } ?>
<?php if ( ZM_WEB_ID_ON_CONSOLE ) { ?>
<th class="colId"><?php echo translate('Id') ?></th>
<th data-sortable="true" data-field="Id" class="colId"><?php echo translate('Id') ?></th>
<?php } ?>
<th class="colName"><i class="material-icons">videocam</i>&nbsp;<?php echo translate('Name') ?></th>
<th class="colFunction"><?php echo translate('Function') ?></th>
<?php if ( ZM_WEB_LIST_THUMBS ) { ?>
<th data-sortable="false" data-field="Thumbnail" class="colThumbnail"><?php echo translate('Thumbnail') ?></th>
<?php } ?>
<th data-sortable="true" data-field="Name" class="colName"><i class="material-icons">videocam</i>&nbsp;<?php echo translate('Name') ?></th>
<th data-sortable="true" data-field="Function" class="colFunction"><?php echo translate('Function') ?></th>
<?php if ( count($Servers) ) { ?>
<th class="colServer"><?php echo translate('Server') ?></th>
<th data-sortable="true" data-field="Server" class="colServer"><?php echo translate('Server') ?></th>
<?php } ?>
<th class="colSource"><i class="material-icons">settings</i>&nbsp;<?php echo translate('Source') ?></th>
<th data-sortable="true" data-field="Source" class="colSource"><i class="material-icons">settings</i>&nbsp;<?php echo translate('Source') ?></th>
<?php if ( $show_storage_areas ) { ?>
<th class="colStorage"><?php echo translate('Storage') ?></th>
<th data-sortable="true" data-field="Storage" class="colStorage"><?php echo translate('Storage') ?></th>
<?php }
foreach ( array_keys($eventCounts) as $i ) {
$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 '<th class="colEvents"><a '
echo '<th data-sortable="true" data-field="'.$i.'Events" class="colEvents"><a '
.(canView('Events') ? 'href="?view='.ZM_WEB_EVENTS_VIEW.'&amp;page=1'.$filter['querystring'].'">' : '')
.$eventCounts[$i]['title']
.'</a></th>'.PHP_EOL;
} // end foreach eventCounts
?>
<th class="colZones"><a href="?view=zones"><?php echo translate('Zones') ?></a></th>
<th data-sortable="true" data-field="ZoneCount" class="colZones"><a href="?view=zones"><?php echo translate('Zones') ?></a></th>
</tr>
</thead>
<tbody id="consoleTableBody">
<?php
$table_head = ob_get_contents();
ob_end_clean();
echo $table_head;
$group_ids_by_monitor_id = array();
foreach (ZM\Group_Monitor::find(array('MonitorId'=>$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 '</table>';
echo $table_head;
} # monitor_i % 200
?>
<tr id="<?php echo 'monitor_id-'.$monitor['Id'] ?>" title="<?php echo $monitor['Id'] ?>">
<?php
$source_class = 'infoText';
$source_class_reason = '';
# 1 minute + fps_report_interval should be plenty.
$fps_report_seconds = 60+($monitor['FPSReportInterval'] * $monitor['CaptureFPS']);
if ( (!$monitor['Status'] || ($monitor['Status'] == 'NotRunning')) && ($monitor['Type'] != 'WebSite')) {
$source_class = 'errorText';
$source_class_reason = translate('Not Running');
} else if ((!$monitor['UpdatedOn']) or (strtotime($monitor['UpdatedOn']) < time()-$fps_report_seconds)) {
$source_class = 'errorText';
$source_class_reason = translate('Offline');
} else {
if ( $monitor['CaptureFPS'] == '0.00' ) {
$source_class = 'errorText';
$source_class_reason = translate('No capture FPS');
} else if ( (!$monitor['AnalysisFPS']) && ($monitor['Analysing'] != 'None') ) {
$source_class = 'warnText';
$source_class_reason = translate('No analysis FPS');
}
}
$function_class = 'infoText';
$dot_class = $source_class;
$dot_class_reason = $source_class_reason;
if ( $function_class != 'infoText' ) {
$dot_class = $function_class;
#} else if (($monitor['Analysing'] == 'Always') and !$monitor['Enabled']) {
#$dot_class .= ' warnText';
#$dot_class_reason .= ' '.translate('Analysis is disabled');
#FIXME replace this with a check for runstate vs dbstate
}
$stream_available = canView('Stream') and $monitor['Type']=='WebSite' or ($monitor['CaptureFPS'] && $monitor['Capturing'] != 'None');
if ($canEditMonitors) {
?>
<td class="colMark">
<input type="checkbox" name="markMids[]" value="<?php echo $monitor['Id'] ?>" data-on-click-this="setButtonStates"/>
</td>
<?php
}
if (ZM_WEB_ID_ON_CONSOLE) {
?>
<td class="colId"><a <?php echo ($stream_available ? 'href="?view=watch&amp;mid='.$monitor['Id'].'">' : '>') . $monitor['Id'] ?></a></td>
<?php
}
$imgHTML = '';
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;' : '';
$imgHTML = '<div class="colThumbnail" style="'.$thmbHeight.'"><a';
$imgHTML .= $stream_available ? ' href="?view=watch&amp;mid='.$monitor['Id'].'">' : '>';
$imgHTML .= '<img id="thumbnail' .$Monitor->Id(). '" src="' .$stillSrc. '" style="'
.$thmbWidth.$thmbHeight. '" stream_src="' .$streamSrc. '" still_src="' .$stillSrc. '"'.
($options['width'] ? ' width="'.$options['width'].'"' : '' ).
($options['height'] ? ' height="'.$options['height'].'"' : '' ).
' loading="lazy" /></a></div>';
}
?>
<td class="colName">
<i class="material-icons <?php echo $dot_class ?>" title="<?php echo $dot_class_reason ?>">lens</i>
<a <?php echo ($stream_available ? 'href="?view=watch&amp;mid='.$monitor['Id'].'">' : '>') . validHtmlStr($monitor['Name']) ?></a><br/>
<?php echo $imgHTML ?>
<div class="small text-nowrap text-muted">
<?php
if (canView('Groups')) {
echo implode('<br/>',
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(' &gt; ', array_map(function($Group){
if (canView('Stream')) {
return '<a href="?view=montagereview&amp;GroupId='.$Group->Id().'">'.validHtmlStr($Group->Name()).'</a>';
} else {
return validHtmlStr($Group->Name());
}
}, $Groups ));
}, $Monitor->GroupIds()));
}
?>
</div></td>
<td class="colFunction">
<!--<a class="functionLnk <?php echo $function_class ?>" data-mid="<?php echo $monitor['Id'] ?>" id="functionLnk-<?php echo $monitor['Id'] ?>" href="#"><?php echo translate('Fn'.$monitor['Function']) ?></a>-->
<?php
if ((!$monitor['UpdatedOn']) or (strtotime($monitor['UpdatedOn']) < time()-$fps_report_seconds)) {
echo translate('Offline').'<br/>';
} else {
echo translate('Status'.$monitor['Status']).'<br/>';
if ($monitor['Analysing'] != 'None') {
echo translate('Analysing') . ': '.translate($monitor['Analysing']).'<br/>';
}
if ($monitor['Recording'] != 'None') {
echo translate('Recording') . ': '.translate($monitor['Recording']) . ($monitor['ONVIF_Event_Listener'] ? ' Use ONVIF' : "") . '<br/>';
}
?><br/>
<div class="small text-nowrap text-muted">
<?php
$fps_string = '';
if (isset($monitor['CaptureFPS'])) {
$fps_string .= $monitor['CaptureFPS'];
}
if ( isset($monitor['AnalysisFPS']) and ($monitor['Analysing'] != 'None')) {
$fps_string .= '/' . $monitor['AnalysisFPS'];
$total_analysis_fps += $monitor['AnalysisFPS'];
}
if ($fps_string) $fps_string .= ' fps';
if (!empty($monitor['CaptureBandwidth']))
$fps_string .= ' ' . human_filesize($monitor['CaptureBandwidth']).'/s';
$total_capturing_bandwidth += $monitor['CaptureBandwidth'];
$total_fps += $monitor['CaptureFPS'];
echo $fps_string;
echo '</div>';
} # end if offline
echo '</td>'.PHP_EOL;
if (count($Servers)) {
$Server = isset($ServersById[$monitor['ServerId']]) ? $ServersById[$monitor['ServerId']] : new ZM\Server($monitor['ServerId']);
echo '<td class="colServer">'.validHtmlStr($Server->Name()).'</td>'.PHP_EOL;
}
echo '<td class="colSource">'. makeLink( '?view=monitor&amp;mid='.$monitor['Id'], '<span class="'.$source_class.'">'.validHtmlStr($Monitor->Source()).'</span>', $Monitor->canEdit());
echo '<br/>'.$Monitor->Width().'x'.$Monitor->Height();
echo '</td>';
if ($show_storage_areas) {
echo '<td class="colStorage">'.
(isset($StorageById[$monitor['StorageId']]) ? validHtmlStr($StorageById[$monitor['StorageId']]->Name()) : ($monitor['StorageId']?'<span class="error">Deleted '.$monitor['StorageId'].'</span>' : '')).'</td>'.PHP_EOL;
}
foreach (array_keys($eventCounts) as $i) {
echo '<td class="colEvents"><a '. (canView('Events') ? 'href="?view='.ZM_WEB_EVENTS_VIEW.'&amp;page=1'.$monitor['eventCounts'][$i]['filter']['querystring'].'">' : '') .
(int)$monitor[$i.'Events'] . '<br/></a><div class="small text-nowrap text-muted">' . human_filesize($monitor[$i.'EventDiskSpace']).'</div></td>'.PHP_EOL;
}
echo '<td class="colZones">'. makeLink('?view=zones&amp;mid='.$monitor['Id'], $monitor['ZoneCount'], canView('Monitors')) .'</td>'.PHP_EOL;
?>
</tr>
<?php
} # end for each monitor
?>
</tbody>
<tfoot>
<tr>
@@ -454,6 +303,9 @@ for ($monitor_i = 0; $monitor_i < count($displayMonitors); $monitor_i += 1) {
<?php } ?>
<?php if ( ZM_WEB_ID_ON_CONSOLE ) { ?>
<td class="colId"><?php echo translate('Total').":".count($displayMonitors) ?></td>
<?php } ?>
<?php if ( ZM_WEB_LIST_THUMBS ) { ?>
<td class="colThumbnail"></td>
<?php } ?>
<td class="colName"></td>
<td class="colFunction"><?php echo human_filesize($total_capturing_bandwidth ).'/s '.

View File

@@ -1,21 +1,228 @@
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");
"use strict";
const table = $j('#consoleTable');
var ajax = null;
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
// 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 (only updates th-inner div)
function updateCell(selector, content) {
var cell = footerRow.find(selector);
if (cell.length) {
// Only update the th-inner div if it exists
var innerDiv = cell.find('.th-inner');
if (innerDiv.length) {
innerDiv.html(content);
} else {
tr.removeClass("danger");
cell.html(content);
}
}
}
if ( checked ) {
// Update monitor count (in Id column if shown)
updateCell('td.colId, th.colId', 'Total: ' + footer.monitor_count);
// Update bandwidth/FPS (in Function column)
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, th.colEvents');
eventPeriods.forEach(function(period, index) {
if (eventCells.length > 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'] + '<br/><div class="small text-nowrap text-muted">' +
footer[period + 'EventDiskSpace'] + '</div>';
link.html(newHtml);
} else {
target.html(footer[period + 'Events'] + '<br/><div class="small text-nowrap text-muted">' +
footer[period + 'EventDiskSpace'] + '</div>');
}
}
});
// 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 = '<a href="?view=watch&amp;mid=' + mid + '">' + mid + '</a>';
} 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 = '<i class="material-icons ' + dot_class + '" title="' + dot_class_reason + '">lens</i> ';
if (stream_available) {
nameHtml += '<a href="?view=watch&amp;mid=' + mid + '">' + row.Name + '</a>';
} else {
nameHtml += row.Name;
}
// Add groups
if (row.Groups) {
nameHtml += '<br/><div class="small text-nowrap text-muted">' + row.Groups + '</div>';
}
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<br/>';
} else {
functionHtml = '';
if (row.Analysing && row.Analysing != 'None') {
functionHtml += 'Analysing: ' + row.Analysing + '<br/>';
}
if (row.Recording && row.Recording != 'None') {
functionHtml += 'Recording: ' + row.Recording;
if (row.ONVIF_Event_Listener) {
functionHtml += ' Use ONVIF';
}
functionHtml += '<br/>';
}
functionHtml += '<br/><div class="small text-nowrap text-muted">';
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 + '</div>';
}
row.Function = functionHtml;
// Format Source column with link and dimensions
var sourceHtml = '';
if (canEdit.Monitors) {
sourceHtml = '<a href="?view=monitor&amp;mid=' + mid + '"><span class="' + source_class + '">' + row.Source + '</span></a>';
} else {
sourceHtml = '<span class="' + source_class + '">' + row.Source + '</span>';
}
sourceHtml += '<br/>' + 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'] = '<a href="?view=' + ZM_WEB_EVENTS_VIEW + '&amp;MonitorId=' + mid + '">' +
row[period + 'Events'] + '</a><br/><div class="small text-nowrap text-muted">' +
row[period + 'EventDiskSpace'] + '</div>';
} else {
row[period + 'Events'] = row[period + 'Events'] + '<br/><div class="small text-nowrap text-muted">' +
row[period + 'EventDiskSpace'] + '</div>';
}
});
// Format Zones column
if (canView.Monitors) {
row.ZoneCount = '<a href="?view=zones&amp;mid=' + mid + '">' + row.ZoneCount + '</a>';
}
});
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) {

View File

@@ -11,22 +11,6 @@ if ( canEdit('System') && ZM_DYN_SHOW_DONATE_REMINDER ) {
}
?>
var showDonatePopup = <?php echo isset($showDonatePopup )?'true':'false' ?>;
var monitors = new Array();
<?php
global $monitors;
foreach ( $monitors as $monitor ) {
?>
monitors[<?php echo $monitor->Id() ?>] = {
'Id': <?php echo $monitor->Id() ?>,
'Name': '<?php echo $monitor->Name() ?>',
'ViewWidth': <?php echo $monitor->ViewWidth() ?>,
'ViewHeight':<?php echo $monitor->ViewHeight() ?>,
'Url': '<?php echo $monitor->UrlToIndex( ZM_MIN_STREAMING_PORT ? ($monitor->Id() + ZM_MIN_STREAMING_PORT) : '') ?>',
'Type': '<?php echo $monitor->Type() ?>',
'Function': '<?php echo $monitor->Function() ?>',
'Enabled': '<?php echo $monitor->Enabled() ?>',
'DecodingEnabled': '<?php echo $monitor->DecodingEnabled() ?>'
};
<?php
}
?>
var ZM_WEB_EVENTS_VIEW = '<?php echo ZM_WEB_EVENTS_VIEW ?>';