mirror of
https://github.com/Cacti/plugin_monitor.git
synced 2026-10-23 19:46:47 +09:00
* Monitor: flex grouping cards to full width, order Tree cards by device count * Monitor: drop inaccurate Site/Template ordering comparison from changelog * CI: install the declared thold dependency so Monitor can install under Cacti's new install-failure reporting * Monitor: remove the filter-status/criticality footer and prompt from the board * Monitor: give the busiest grouping cards (span 3-4) a full row so their tiles span the page * Monitor: changelog for full-width busy cards and footer removal * Monitor: drop Site grouping header, tighten tile uptime spacing, add List column tips * Monitor: add clickable List-view thold/servcheck/ping/criticality pills with filter toggles * Monitor: unit tests for new filter branches and thold/servcheck count helpers * Monitor: selectable status pill, fallback/empty pills, hide absent plugin columns, tile sizing, dialog UX * Monitor: center and keep clickable the tile hostname, remove 'Names only' view option (backend retained) * Monitor: multi-select Events filter + Events column (additive), Status becomes scope-only Show * Monitor: apply the Events multi-select on dropdown close (and on change for native selects) * Add All criticality, Device Template multi-select, servcheck scope - Add an "All" option to the Criticality filter. Its value (-1) matches the default and the pill "clear to default" action, which previously set the select to a non-existent option and submitted crit=null, tripping validate_store_request_vars. - Convert the Device Template filter to a checkbox multi-select (like Events) and actually apply it: renderWhereJoin now adds h.host_template_id IN (...) for the template grouping (0 = Non-Templated). Previously the template selection was never applied, so all hosts returned. - Persist the template selection as a user setting and serialize the multi-value for request/save payloads. - Add a "Devices without Service Checks" scope to the Show filter, shown only when the servcheck plugin is installed and permitted. - Tests + regenerated .pot. * Update template escaping assertions for the multiselect markup * Cover empty-template branch in renderWhereJoin * Left-align Actions glyph and harden Device Detail Panels dialog layout - Left-align the List-view Actions column header and fa-layer-group glyph. - Stack the detail panels as plain blocks and force the jQuery UI dialog content box to always fill the dialog width, so the panels can no longer collapse into a narrow right-hand strip. * Size the detail dialog to its content instead of forcing overflow The #monitorPanelDialog overflow override (higher specificity than the theme's .ui-dialog .ui-dialog-content) turned the content into a fixed-height scroll box that fought the theme's overflow:visible and squashed the panels. Use height:auto (capped by maxHeight) and stop overriding overflow so the dialog sizes to its content and the theme governs scrolling. * Anchor the detail dialog 300px from the top instead of centering * Make the detail panels dialog non-modal * Move Notes out of the List view into the detail dialog The Notes column consumed a lot of horizontal space in List view. Drop it from the table and surface the notes in the Host Information panel of the Device Detail Panels dialog instead (shown only when the device has notes). * Add device-edit action icon and Devices filter button (realm-gated) List view: add a pen-square action icon linking to host.php?action=edit for users with the host.php (device edit) realm, beside the detail-panels icon. Filter bar: add a Devices button (right of Acknowledge) that opens the Cacti Devices page, shown only to users with the same realm. Also move the detail dialog anchor from top+300 to top+150. * Anchor the detail dialog at top+80 * List view: replace Hostname column with a Graphs action glyph Remove the Hostname column and add an orange fas fa-chart-line Graphs glyph (.monitorGraph, matching HMIB's colour) to the Actions column, linking to the device's graphs where the hostname hyperlink used to point. * List columns + filter layout refinements - List columns reordered to Description, Location, Site, ID; add a Location column; Averages, Length in Status, Last Fail are now the last three columns, all right-aligned. - rfilter Search now also matches the device location. - Filter: move the Events multiselect to the second row (right of Search) and the Rows dropdown to the second row (left of Refresh); rename that dropdown from Devices to Rows. - Add a renderWhereJoin test for the description/location rfilter search. * Monitor: realm-gate service-check scope/column, persist events+template, legacy status reset, failed_svc independent, defer list counts, pill keyboard a11y, PHPDoc/changelog fixes
1596 lines
47 KiB
PHP
1596 lines
47 KiB
PHP
<?php
|
|
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDTool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
/**
|
|
* Render default monitor grouping output with active filters applied.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderDefault(): string {
|
|
global $maxchars;
|
|
|
|
$result = '';
|
|
|
|
$sql_where = '';
|
|
$sql_join = '';
|
|
$sql_limit = '';
|
|
$sql_order = 'ORDER BY description';
|
|
|
|
$rows = get_request_var('rows');
|
|
|
|
if ($rows == '-1') {
|
|
$rows = read_user_setting('monitor_rows');
|
|
}
|
|
|
|
if (!is_numeric($rows)) {
|
|
$rows = read_config_option('num_rows_table');
|
|
}
|
|
|
|
if (get_request_var('view') == 'list') {
|
|
$sql_order = get_order_string();
|
|
}
|
|
|
|
renderWhereJoin($sql_where, $sql_join);
|
|
|
|
$poller_interval = read_config_option('poller_interval');
|
|
|
|
// Only the List view paginates; all other views show every matching device.
|
|
if (get_request_var('view') == 'list') {
|
|
$sql_limit = ' LIMIT ' . ($rows * (get_request_var('page') - 1)) . ',' . $rows;
|
|
}
|
|
|
|
$hosts_sql = ("SELECT DISTINCT h.*, IFNULL(s.name,' " . __('Non-Site Device', 'monitor') . " ') AS site_name,
|
|
CAST(IF(availability_method = 0, '0',
|
|
IF(status_event_count > 0 AND status IN (1, 2), status_event_count*$poller_interval,
|
|
IF(UNIX_TIMESTAMP(status_rec_date) < 943916400 AND status IN (0, 3), total_polls*$poller_interval,
|
|
IF(UNIX_TIMESTAMP(status_rec_date) > 943916400, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(status_rec_date),
|
|
IF(snmp_sysUptimeInstance>0 AND snmp_version > 0, snmp_sysUptimeInstance/100, UNIX_TIMESTAMP()
|
|
))))) AS unsigned) AS instate
|
|
FROM host AS h
|
|
LEFT JOIN sites AS s
|
|
ON h.site_id = s.id
|
|
$sql_join
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$hosts = db_fetch_assoc($hosts_sql);
|
|
|
|
$total_rows = db_fetch_cell("SELECT COUNT(DISTINCT h.id)
|
|
FROM host AS h
|
|
LEFT JOIN sites AS s
|
|
ON h.site_id = s.id
|
|
$sql_join
|
|
$sql_where");
|
|
|
|
if (cacti_sizeof($hosts)) {
|
|
// Determine the correct width of the cell
|
|
$maxlen = 10;
|
|
|
|
if (get_request_var('view') == 'default') {
|
|
$maxlen = (int) db_fetch_cell("SELECT MAX(LENGTH(description))
|
|
FROM host AS h
|
|
$sql_join
|
|
$sql_where");
|
|
}
|
|
|
|
$maxlen = getMonitorTrimLength($maxlen);
|
|
|
|
$function = 'renderHeader' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
// Call the custom render_header_ function
|
|
$result .= $function($total_rows, $rows);
|
|
}
|
|
|
|
$count = 0;
|
|
|
|
foreach ($hosts as $host) {
|
|
if (is_device_allowed($host['id'])) {
|
|
$result .= renderHost($host, true, $maxlen);
|
|
}
|
|
|
|
$count++;
|
|
}
|
|
|
|
$function = 'renderFooter' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
// Call the custom render_footer_ function
|
|
$result .= $function($total_rows, $rows);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Whether the current view uses the device-tile card layout (as opposed to the
|
|
* name grid or paginated list), and so supports grouped card panels.
|
|
*
|
|
* @return bool
|
|
*/
|
|
function monitorUseCardLayout(): bool {
|
|
return in_array(get_request_var('view'), ['default', 'tiles', 'tilesadt'], true);
|
|
}
|
|
|
|
/**
|
|
* Resolve the worst (most severe) device status CSS class across a group so the
|
|
* group's card title can be coloured to match (down/breached/triggered/etc.).
|
|
*
|
|
* @param array $hosts Host rows in the group.
|
|
*
|
|
* @return string Status CSS class (e.g. deviceDown), defaulting to deviceUp.
|
|
*/
|
|
function monitorGroupWorstStatusClass(array $hosts): string {
|
|
global $iclasses;
|
|
|
|
$rank = [1 => 100, 8 => 90, 4 => 80, 10 => 75, 9 => 70, 5 => 65, 7 => 60, 2 => 50, 0 => 40, 6 => 30, 3 => 10];
|
|
$worst = 3;
|
|
$worst_val = -1;
|
|
|
|
foreach ($hosts as $host) {
|
|
$status = getHostStatus($host);
|
|
$value = $rank[$status] ?? 0;
|
|
|
|
if ($value > $worst_val) {
|
|
$worst_val = $value;
|
|
$worst = $status;
|
|
}
|
|
}
|
|
|
|
return $iclasses[$worst] ?? 'deviceUp';
|
|
}
|
|
|
|
/**
|
|
* Load the saved card ordering for a grouping from the active dashboard's
|
|
* properties, falling back to the per-user setting.
|
|
*
|
|
* @param string $grouping Grouping key (site/template).
|
|
*
|
|
* @return array Ordered list of group ids, or empty when none saved.
|
|
*/
|
|
function monitorGetCardOrder(string $grouping): array {
|
|
$dashboard = get_request_var('dashboard');
|
|
|
|
if ($dashboard > 0 && monitorDashboardOwnedByUser($dashboard)) {
|
|
$props = monitorGetDashboardProperties($dashboard);
|
|
|
|
if (isset($props['cardorder'][$grouping]) && is_array($props['cardorder'][$grouping])) {
|
|
return $props['cardorder'][$grouping];
|
|
}
|
|
}
|
|
|
|
$json = read_user_setting('monitor_cardorder');
|
|
|
|
if ($json != '') {
|
|
$order = json_decode($json, true);
|
|
|
|
if (isset($order[$grouping]) && is_array($order[$grouping])) {
|
|
return $order[$grouping];
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Reorder a group map to honour a saved id ordering, appending any groups not
|
|
* present in the saved order.
|
|
*
|
|
* @param array $groups Map of group id => group data.
|
|
* @param array $order Saved list of group ids.
|
|
*
|
|
* @return array Reordered group map.
|
|
*/
|
|
function monitorApplyCardOrder(array $groups, array $order): array {
|
|
if (!cacti_sizeof($order)) {
|
|
return $groups;
|
|
}
|
|
|
|
$ordered = [];
|
|
|
|
foreach ($order as $gid) {
|
|
$gid = (string) $gid;
|
|
|
|
if (isset($groups[$gid])) {
|
|
$ordered[$gid] = $groups[$gid];
|
|
unset($groups[$gid]);
|
|
}
|
|
}
|
|
|
|
foreach ($groups as $gid => $group) {
|
|
$ordered[$gid] = $group;
|
|
}
|
|
|
|
return $ordered;
|
|
}
|
|
|
|
/**
|
|
* Render a set of grouped device cards (one card/panel per group) in a
|
|
* responsive grid, with saved ordering applied and each title coloured by the
|
|
* group's worst device status.
|
|
*
|
|
* @param string $grouping Grouping key (site/template).
|
|
* @param array $groups Map of group id => ['label' => string, 'hosts' => array].
|
|
* @param int $maxlen Trim length for host titles.
|
|
*
|
|
* @return string
|
|
*/
|
|
function monitorRenderGroupCards(string $grouping, array $groups, int $maxlen): string {
|
|
$groups = monitorApplyCardOrder($groups, monitorGetCardOrder($grouping));
|
|
|
|
$out = "<div class='monitorGroupGrid' data-grouping='" . html_escape($grouping) . "'>";
|
|
|
|
foreach ($groups as $gid => $group) {
|
|
$worst = monitorGroupWorstStatusClass($group['hosts']);
|
|
|
|
// Let groups with many devices take a wider card so their tiles flow
|
|
// across the page instead of stacking into one tall, narrow column. The
|
|
// span maps to flex sizing in css (see .monitorGroupCard[data-span]).
|
|
$span = (int) min(4, max(1, (int) ceil(cacti_sizeof($group['hosts']) / 6)));
|
|
|
|
// Size every tile to the longest (trimmed) description in view so long
|
|
// hostnames never overlap their neighbours. css widens the titles from
|
|
// this custom property.
|
|
$title_ch = max(8, min(40, $maxlen));
|
|
$body = "<div class='monitor_container' style='--monitor-title-ch:$title_ch'>";
|
|
|
|
foreach ($group['hosts'] as $host) {
|
|
$body .= renderHost($host, true, $maxlen);
|
|
}
|
|
|
|
$body .= '</div>';
|
|
|
|
$out .= "<div class='monitorPanel monitorGroupCard' data-group='" . html_escape((string) $gid) . "' data-span='$span'>
|
|
<div class='monitorPanelHeader monitorGroupTitle $worst'><i class='fas fa-grip-vertical monitorGroupDrag'></i> " . html_escape($group['label']) . "</div>
|
|
<div class='monitorPanelBody'>$body</div>
|
|
</div>";
|
|
}
|
|
|
|
$out .= '</div>';
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Render host output grouped by site.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderSite(): string {
|
|
global $maxchars;
|
|
|
|
$result = '';
|
|
|
|
$sql_where = '';
|
|
$sql_join = '';
|
|
$sql_limit = '';
|
|
|
|
$rows = get_request_var('rows');
|
|
|
|
if ($rows == '-1') {
|
|
$rows = read_user_setting('monitor_rows');
|
|
}
|
|
|
|
if (!is_numeric($rows)) {
|
|
$rows = read_config_option('num_rows_table');
|
|
}
|
|
|
|
renderWhereJoin($sql_where, $sql_join);
|
|
|
|
// Only the List view paginates; all other views show every matching device.
|
|
if (get_request_var('view') == 'list') {
|
|
$sql_limit = ' LIMIT ' . ($rows * (get_request_var('page') - 1)) . ',' . $rows;
|
|
}
|
|
|
|
$hosts_sql = ("SELECT DISTINCT h.*, IFNULL(s.name,' " . __('Non-Site Devices', 'monitor') . " ') AS site_name
|
|
FROM host AS h
|
|
LEFT JOIN sites AS s
|
|
ON s.id = h.site_id
|
|
$sql_join
|
|
$sql_where
|
|
ORDER BY site_name, description
|
|
$sql_limit");
|
|
|
|
$hosts = db_fetch_assoc($hosts_sql);
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
return $result;
|
|
}
|
|
|
|
[$hosts, $host_ids] = monitorFilterAllowedHosts($hosts);
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
return $result;
|
|
}
|
|
|
|
$maxlen = 10;
|
|
|
|
if (get_request_var('view') == 'default' && cacti_sizeof($host_ids)) {
|
|
$maxlen = (int) db_fetch_cell('SELECT MAX(LENGTH(description))
|
|
FROM host AS h
|
|
WHERE id IN (' . implode(',', $host_ids) . ')');
|
|
}
|
|
|
|
$maxlen = getMonitorTrimLength($maxlen);
|
|
|
|
if (monitorUseCardLayout()) {
|
|
$groups = [];
|
|
|
|
foreach ($hosts as $host) {
|
|
$gid = (string) $host['site_id'];
|
|
|
|
if (!isset($groups[$gid])) {
|
|
$groups[$gid] = ['label' => $host['site_name'], 'hosts' => []];
|
|
}
|
|
|
|
$groups[$gid]['hosts'][] = $host;
|
|
}
|
|
|
|
return monitorRenderGroupCards('site', $groups, $maxlen);
|
|
}
|
|
|
|
$function = 'renderHeader' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
$result .= $function();
|
|
}
|
|
|
|
foreach ($hosts as $host) {
|
|
$result .= renderHost($host, true, $maxlen);
|
|
}
|
|
|
|
$function = 'renderFooter' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
$result .= $function();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Render host output grouped by host template.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderTemplate(): string {
|
|
global $maxchars;
|
|
|
|
$result = '';
|
|
|
|
$sql_where = '';
|
|
$sql_join = '';
|
|
$sql_limit = '';
|
|
|
|
$rows = get_request_var('rows');
|
|
|
|
if ($rows == '-1') {
|
|
$rows = read_user_setting('monitor_rows');
|
|
}
|
|
|
|
if (!is_numeric($rows)) {
|
|
$rows = read_config_option('num_rows_table');
|
|
}
|
|
|
|
renderWhereJoin($sql_where, $sql_join);
|
|
|
|
// Only the List view paginates; all other views show every matching device.
|
|
if (get_request_var('view') == 'list') {
|
|
$sql_limit = ' LIMIT ' . ($rows * (get_request_var('page') - 1)) . ',' . $rows;
|
|
}
|
|
|
|
if (get_request_var('template') > 0) {
|
|
$sql_where .= ($sql_where == '' ? '' : 'AND ') . 'ht.id = ' . get_request_var('template');
|
|
}
|
|
|
|
$sql_template = 'INNER JOIN host_template AS ht ON h.host_template_id=ht.id ';
|
|
|
|
if (get_request_var('template') == -2) {
|
|
$sql_where .= ($sql_where == '' ? '' : 'AND ') . 'ht.id IS NULL';
|
|
$sql_template = 'LEFT JOIN host_template AS ht ON h.host_template_id=ht.id ';
|
|
}
|
|
|
|
$hosts = db_fetch_assoc("SELECT DISTINCT
|
|
h.*, ht.name AS host_template_name
|
|
FROM host AS h
|
|
$sql_template
|
|
$sql_join
|
|
$sql_where
|
|
ORDER BY ht.name, h.description
|
|
$sql_limit");
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
return $result;
|
|
}
|
|
|
|
[$hosts, $host_ids] = monitorFilterAllowedHosts($hosts);
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
return $result;
|
|
}
|
|
|
|
$maxlen = 10;
|
|
|
|
if (get_request_var('view') == 'default' && cacti_sizeof($host_ids)) {
|
|
$maxlen = (int) db_fetch_cell('SELECT MAX(LENGTH(description))
|
|
FROM host AS h
|
|
WHERE id IN (' . implode(',', $host_ids) . ')');
|
|
}
|
|
|
|
$maxlen = getMonitorTrimLength($maxlen);
|
|
|
|
if (monitorUseCardLayout()) {
|
|
$groups = [];
|
|
|
|
foreach ($hosts as $host) {
|
|
$gid = (string) $host['host_template_id'];
|
|
$label = $host['host_template_name'] != '' ? $host['host_template_name'] : __('Non-Templated Devices', 'monitor');
|
|
|
|
if (!isset($groups[$gid])) {
|
|
$groups[$gid] = ['label' => $label, 'hosts' => []];
|
|
}
|
|
|
|
$groups[$gid]['hosts'][] = $host;
|
|
}
|
|
|
|
return monitorRenderGroupCards('template', $groups, $maxlen);
|
|
}
|
|
|
|
$function = 'renderHeader' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
$result .= $function();
|
|
}
|
|
|
|
foreach ($hosts as $host) {
|
|
$result .= renderHost($host, true, $maxlen);
|
|
}
|
|
|
|
$function = 'renderFooter' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
$result .= $function();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Filter out disallowed hosts and return normalized host/id lists.
|
|
*
|
|
* @param array $hosts Host rows.
|
|
*
|
|
* @return array{array, array}
|
|
*/
|
|
function monitorFilterAllowedHosts(array $hosts): array {
|
|
$host_ids = [];
|
|
|
|
foreach ($hosts as $index => $host) {
|
|
if (is_device_allowed($host['id'])) {
|
|
$host_ids[] = $host['id'];
|
|
} else {
|
|
unset($hosts[$index]);
|
|
}
|
|
}
|
|
|
|
return [array_values($hosts), $host_ids];
|
|
}
|
|
|
|
/**
|
|
* Determine max description trim length for tree rendering context.
|
|
*
|
|
* @return int
|
|
*/
|
|
function monitorGetTreeRenderMaxLength(): int {
|
|
$maxlen = 10;
|
|
|
|
if (get_request_var('view') == 'default') {
|
|
$maxlen = db_fetch_cell("SELECT MAX(LENGTH(description))
|
|
FROM host AS h
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON gti.host_id = h.id
|
|
WHERE disabled = ''
|
|
AND deleted = ''");
|
|
}
|
|
|
|
return getMonitorTrimLength($maxlen);
|
|
}
|
|
|
|
/**
|
|
* Build map of tree branch labels keyed by "tree_id:parent_id".
|
|
*
|
|
* @param array $branchWhost Tree branch/host rows.
|
|
*
|
|
* @return array
|
|
*/
|
|
function monitorBuildTreeTitles(array $branchWhost): array {
|
|
$titles = [];
|
|
$ptree = '';
|
|
|
|
foreach ($branchWhost as $b) {
|
|
if ($ptree != $b['graph_tree_id']) {
|
|
$titles[$b['graph_tree_id'] . ':0'] = __('Root Branch', 'monitor');
|
|
$ptree = $b['graph_tree_id'];
|
|
}
|
|
|
|
if ($b['parent'] > 0) {
|
|
$titles[$b['graph_tree_id'] . ':' . $b['parent']] = db_fetch_cell_prepared(
|
|
'SELECT title
|
|
FROM graph_tree_items
|
|
WHERE id = ?
|
|
AND graph_tree_id = ?
|
|
ORDER BY position',
|
|
[$b['parent'], $b['graph_tree_id']]
|
|
);
|
|
}
|
|
}
|
|
|
|
return $titles;
|
|
}
|
|
|
|
/**
|
|
* Render grouped tree title/branch sections for monitor view.
|
|
*
|
|
* @param array $titles Tree titles map keyed by "tree_id:parent_id".
|
|
* @param int $maxlen Trim length used for host title rendering.
|
|
*
|
|
* @return string
|
|
*/
|
|
function monitorRenderTreeTitleSections(array $titles, int $maxlen): string {
|
|
$result = '';
|
|
$ptree = '';
|
|
|
|
foreach ($titles as $index => $title) {
|
|
[$graph_tree_id, $parent] = explode(':', $index);
|
|
$oid = $parent;
|
|
|
|
$sql_where = '';
|
|
$sql_join = '';
|
|
renderWhereJoin($sql_where, $sql_join);
|
|
|
|
$hosts_sql = "SELECT h.*, IFNULL(s.name,' " . __('Non-Site Device', 'monitor') . " ') AS site_name
|
|
FROM host AS h
|
|
LEFT JOIN sites AS s
|
|
ON h.site_id = s.id
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON h.id = gti.host_id
|
|
$sql_join
|
|
$sql_where
|
|
AND parent = ?
|
|
AND graph_tree_id = ?
|
|
GROUP BY h.id
|
|
ORDER BY gti.position";
|
|
|
|
$hosts = db_fetch_assoc_prepared($hosts_sql, [$oid, $graph_tree_id]);
|
|
|
|
$tree_name = db_fetch_cell_prepared(
|
|
'SELECT name
|
|
FROM graph_tree
|
|
WHERE id = ?',
|
|
[$graph_tree_id]
|
|
);
|
|
|
|
if ($ptree != $tree_name) {
|
|
if ($ptree != '') {
|
|
$result .= '</div>';
|
|
}
|
|
|
|
$result .= "<div class='monitorTableHeader'>
|
|
<div class='navBarNavigation'>
|
|
<div class='navBarNavigationNone'>" . __esc('Tree: %s', $tree_name, 'monitor') . "</div>
|
|
</div>
|
|
</div>
|
|
<div class='monitorTable'>
|
|
<div class='monitor_sub_container'>";
|
|
|
|
$ptree = $tree_name;
|
|
}
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
continue;
|
|
}
|
|
|
|
[$hosts] = monitorFilterAllowedHosts($hosts);
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
continue;
|
|
}
|
|
|
|
$result .= "<div class='monitorSubTable'><div class='navBarNavigation'><div class='navBarNavigationNone'>" . __esc('Branch: %s', $title, 'monitor') . "</div></div><div class='monitor_sub_container'>";
|
|
|
|
foreach ($hosts as $host) {
|
|
$result .= renderHost($host, true, $maxlen);
|
|
}
|
|
|
|
$result .= '</div></div>';
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Translatable label for the card/section holding devices not on any tree.
|
|
*
|
|
* @return string
|
|
*/
|
|
function monitorNonTreeLabel(): string {
|
|
return __('Non-Tree Devices', 'monitor');
|
|
}
|
|
|
|
/**
|
|
* Render section for monitored hosts that are not attached to any tree.
|
|
*
|
|
* @return string
|
|
*/
|
|
function monitorRenderNonTreeSection(): string {
|
|
$result = '';
|
|
|
|
if (get_request_var('tree') >= 0) {
|
|
return $result;
|
|
}
|
|
|
|
$hosts = getHostNonTreeArray();
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
return $result;
|
|
}
|
|
|
|
[$hosts, $host_ids] = monitorFilterAllowedHosts($hosts);
|
|
|
|
if (!cacti_sizeof($hosts)) {
|
|
return $result;
|
|
}
|
|
|
|
$maxlen = 10;
|
|
|
|
if (get_request_var('view') == 'default' && cacti_sizeof($host_ids)) {
|
|
$maxlen = db_fetch_cell('SELECT MAX(LENGTH(description))
|
|
FROM host AS h
|
|
WHERE id IN (' . implode(',', $host_ids) . ")
|
|
AND h.deleted = ''");
|
|
}
|
|
|
|
$maxlen = getMonitorTrimLength($maxlen);
|
|
|
|
$result .= "<div class='monitorTableHeader'>
|
|
<div class='navBarNavigation'>
|
|
<div class='navBarNavigationNone'>" . monitorNonTreeLabel() . "</div>
|
|
</div>
|
|
</div>
|
|
<div class='monitor_container'>";
|
|
|
|
foreach ($hosts as $leaf) {
|
|
$result .= renderHost($leaf, true, $maxlen);
|
|
}
|
|
|
|
$result .= '</div></div>';
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Render tree grouping as device cards (one card per tree, plus a Non-Tree
|
|
* card), mirroring the Site and Device Template card layouts.
|
|
*
|
|
* @param array $tree_list Allowed trees for the current filter.
|
|
*
|
|
* @return string
|
|
*/
|
|
function monitorRenderTreeCards(array $tree_list): string {
|
|
$groups = [];
|
|
$host_ids = [];
|
|
|
|
if (cacti_sizeof($tree_list)) {
|
|
$tree_ids = [];
|
|
|
|
foreach ($tree_list as $tree) {
|
|
$tree_ids[$tree['id']] = $tree['id'];
|
|
}
|
|
|
|
$sql_where = '';
|
|
$sql_join = '';
|
|
renderWhereJoin($sql_where, $sql_join);
|
|
|
|
$hosts = db_fetch_assoc('SELECT DISTINCT h.*, gt.id AS graph_tree_id, gt.name AS graph_tree_name
|
|
FROM host AS h
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON h.id = gti.host_id
|
|
INNER JOIN graph_tree AS gt
|
|
ON gt.id = gti.graph_tree_id
|
|
' . $sql_join . '
|
|
' . $sql_where . '
|
|
AND gti.host_id > 0
|
|
AND gti.graph_tree_id IN (' . implode(',', $tree_ids) . ')
|
|
ORDER BY gt.sequence, h.description');
|
|
|
|
if (cacti_sizeof($hosts)) {
|
|
[$hosts, $ids] = monitorFilterAllowedHosts($hosts);
|
|
|
|
$host_ids = array_merge($host_ids, $ids);
|
|
|
|
foreach ($hosts as $host) {
|
|
$gid = 'tree_' . $host['graph_tree_id'];
|
|
|
|
if (!isset($groups[$gid])) {
|
|
$groups[$gid] = ['label' => $host['graph_tree_name'], 'hosts' => []];
|
|
}
|
|
|
|
$groups[$gid]['hosts'][] = $host;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Devices not attached to any tree get their own card, unless a specific
|
|
// tree is selected in the filter.
|
|
if (get_request_var('tree') < 0) {
|
|
$nontree = getHostNonTreeArray();
|
|
|
|
if (cacti_sizeof($nontree)) {
|
|
[$nontree, $ids] = monitorFilterAllowedHosts($nontree);
|
|
|
|
if (cacti_sizeof($nontree)) {
|
|
$host_ids = array_merge($host_ids, $ids);
|
|
|
|
$groups['nontree'] = ['label' => monitorNonTreeLabel(), 'hosts' => $nontree];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!cacti_sizeof($groups)) {
|
|
return '';
|
|
}
|
|
|
|
// Size the trim length from the hosts actually rendered here (including the
|
|
// Non-Tree card) so labels aren't trimmed against unrelated tree devices.
|
|
$maxlen = 10;
|
|
|
|
if (get_request_var('view') == 'default' && cacti_sizeof($host_ids)) {
|
|
$maxlen = (int) db_fetch_cell('SELECT MAX(LENGTH(description))
|
|
FROM host AS h
|
|
WHERE id IN (' . implode(',', $host_ids) . ')');
|
|
}
|
|
|
|
$maxlen = getMonitorTrimLength($maxlen);
|
|
|
|
// Default-order the tree cards by device count (busiest first); any saved
|
|
// drag order the user set still wins in monitorRenderGroupCards().
|
|
uasort($groups, static function (array $a, array $b): int {
|
|
return cacti_sizeof($b['hosts']) <=> cacti_sizeof($a['hosts']);
|
|
});
|
|
|
|
return monitorRenderGroupCards('tree', $groups, $maxlen);
|
|
}
|
|
|
|
/**
|
|
* Render monitor tree grouping view, including tree and non-tree sections.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderTree(): string {
|
|
$result = '';
|
|
|
|
if (get_request_var('tree') > 0) {
|
|
$sql_where = 'gt.id=' . get_request_var('tree');
|
|
} else {
|
|
$sql_where = '';
|
|
}
|
|
|
|
if (get_request_var('tree') != -2) {
|
|
$tree_list = get_allowed_trees(false, false, $sql_where, 'sequence');
|
|
} else {
|
|
$tree_list = [];
|
|
}
|
|
|
|
// Card-based views group each tree (and non-tree devices) into panels, the
|
|
// same as the Site and Device Template groupings.
|
|
if (monitorUseCardLayout()) {
|
|
return monitorRenderTreeCards($tree_list);
|
|
}
|
|
|
|
$function = 'renderHeader' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
$hosts = [];
|
|
|
|
// Call the custom render_header_ function
|
|
$result .= $function();
|
|
}
|
|
|
|
if (cacti_sizeof($tree_list)) {
|
|
$tree_ids = [];
|
|
|
|
foreach ($tree_list as $tree) {
|
|
$tree_ids[$tree['id']] = $tree['id'];
|
|
}
|
|
|
|
$sql_where = '';
|
|
$sql_join = '';
|
|
renderWhereJoin($sql_where, $sql_join);
|
|
|
|
$branchWhost = db_fetch_assoc("SELECT DISTINCT gti.graph_tree_id, gti.parent
|
|
FROM graph_tree_items AS gti
|
|
INNER JOIN graph_tree AS gt
|
|
ON gt.id = gti.graph_tree_id
|
|
INNER JOIN host AS h
|
|
ON h.id = gti.host_id
|
|
$sql_join
|
|
$sql_where
|
|
AND gti.host_id > 0
|
|
AND gti.graph_tree_id IN (" . implode(',', $tree_ids) . ')
|
|
ORDER BY gt.sequence, gti.position');
|
|
|
|
if (cacti_sizeof($branchWhost)) {
|
|
$titles = monitorBuildTreeTitles($branchWhost);
|
|
$result .= monitorRenderTreeTitleSections($titles, monitorGetTreeRenderMaxLength());
|
|
}
|
|
|
|
$result .= '</div>';
|
|
}
|
|
|
|
$result .= monitorRenderNonTreeSection();
|
|
|
|
$function = 'renderFooter' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
// Call the custom render_footer_ function
|
|
$result .= $function();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Resolve display status for a host with monitor/thold/mute overlays applied.
|
|
*
|
|
* @param array $host Host row data.
|
|
* @param bool $real Return raw computed status even if icon class is missing.
|
|
*
|
|
* @return int
|
|
*/
|
|
function getHostStatus(array $host, bool $real = false): int {
|
|
global $thold_hosts, $servcheck_hosts, $iclasses;
|
|
|
|
// If the host has been muted, show the muted Icon
|
|
if ($host['status'] != 1 && in_array($host['id'], $thold_hosts, true)) {
|
|
$host['status'] = 4;
|
|
}
|
|
|
|
if (in_array($host['id'], $_SESSION['monitor_muted_hosts'], true) && $host['status'] == 1) {
|
|
$host['status'] = 5;
|
|
} elseif (in_array($host['id'], $_SESSION['monitor_muted_hosts'], true) && $host['status'] == 4) {
|
|
$host['status'] = 9;
|
|
} elseif ($host['status'] == 3) {
|
|
if ($host['cur_time'] > $host['monitor_alert'] && !empty($host['monitor_alert'])) {
|
|
$host['status'] = 8;
|
|
} elseif ($host['cur_time'] > $host['monitor_warn'] && !empty($host['monitor_warn'])) {
|
|
$host['status'] = 7;
|
|
} elseif (isset($servcheck_hosts) && array_key_exists($host['id'], $servcheck_hosts)) {
|
|
$host['status'] = 10;
|
|
}
|
|
}
|
|
|
|
// If wanting the real status, or the status is already known
|
|
// return the real status, otherwise default to unknown
|
|
return ($real || array_key_exists($host['status'], $iclasses)) ? $host['status'] : 0;
|
|
}
|
|
|
|
/**
|
|
* Translate status code into localized display label.
|
|
*
|
|
* @param int|string $status Monitor status code.
|
|
*
|
|
* @return string
|
|
*/
|
|
function getHostStatusDescription(mixed $status): string {
|
|
global $icolorsdisplay;
|
|
|
|
if (array_key_exists($status, $icolorsdisplay)) {
|
|
return $icolorsdisplay[$status];
|
|
} else {
|
|
return __('Unknown', 'monitor') . " ($status)";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render one host using view-specific renderer or default tile layout.
|
|
*
|
|
* @param array $host Host row data.
|
|
* @param bool $float Legacy compatibility flag (currently unused).
|
|
* @param int $maxlen Maximum host description trim length.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
function renderHost(array $host, bool $float = true, int $maxlen = 10): ?string {
|
|
global $thold_hosts, $config, $icolorsdisplay, $iclasses, $classes, $maxchars, $mon_zoom_state;
|
|
|
|
// throw out tree root items
|
|
if (array_key_exists('name', $host)) {
|
|
return null;
|
|
}
|
|
|
|
if ($host['id'] <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$host['anchor'] = $config['url_path'] . 'graph_view.php?action=preview&reset=1&host_id=' . $host['id'];
|
|
|
|
// Preserve the raw availability status (0-3) before it is collapsed into the
|
|
// single display status, so the Events column can detect every active event.
|
|
$host['raw_status'] = (int) $host['status'];
|
|
|
|
if ($host['status'] == 3 && array_key_exists($host['id'], $thold_hosts)) {
|
|
$host['status'] = 4;
|
|
$host['anchor'] = $config['url_path'] . 'plugins/thold/thold_graph.php?action=thold&reset=true&status=1&host_id=' . $host['id'];
|
|
}
|
|
|
|
$host['real_status'] = getHostStatus($host, true);
|
|
$host['status'] = getHostStatus($host);
|
|
$host['iclass'] = $iclasses[$host['status']];
|
|
|
|
$function = 'renderHost' . ucfirst(get_request_var('view'));
|
|
|
|
if (function_exists($function)) {
|
|
// Call the custom render_host_ function
|
|
$result = $function($host);
|
|
} else {
|
|
$iclass = getStatusIcon($host['status'], $host['monitor_icon']);
|
|
$fclass = get_request_var('size');
|
|
|
|
$monitor_times = read_user_setting('monitor_uptime');
|
|
$monitor_time_html = '';
|
|
|
|
if ($host['status'] <= 2 || $host['status'] == 5) {
|
|
if ($mon_zoom_state) {
|
|
$fclass = 'monitor_errorzoom';
|
|
}
|
|
$tis = get_timeinstate($host);
|
|
|
|
if ($monitor_times == 'on') {
|
|
$monitor_time_html = "<br><span class='monitor_device{$fclass} deviceDown monitor_uptime'>$tis</span>";
|
|
}
|
|
$result = "<div class='$fclass flash monitor_device_frame'><a class='pic hyperLink' href='" . html_escape($host['anchor']) . "'><i id='" . $host['id'] . "' class='$iclass " . $host['iclass'] . "'></i><br><span class='{$fclass}_title'>" . title_trim(html_escape($host['description']), $maxlen) . "</span>$monitor_time_html</a></div>";
|
|
} else {
|
|
$tis = get_uptime($host);
|
|
|
|
if ($monitor_times == 'on') {
|
|
$monitor_time_html = "<br><div class='monitor_device{$fclass} deviceUp monitor_uptime'>$tis</div>";
|
|
}
|
|
|
|
$result = "<div class='$fclass monitor_device_frame'><a class='pic hyperLink' href='" . html_escape($host['anchor']) . "'><i id=" . $host['id'] . " class='$iclass " . $host['iclass'] . "'></i><br><span class='{$fclass}_title'>" . title_trim(html_escape($host['description']), $maxlen) . "</span>$monitor_time_html</a></div>";
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Resolve icon class for host status and configured monitor icon.
|
|
*
|
|
* @param int $status Current monitor status.
|
|
* @param string $icon Configured icon key.
|
|
*
|
|
* @return string
|
|
*/
|
|
function getStatusIcon(int $status, string $icon): string {
|
|
global $fa_icons;
|
|
|
|
if (($status == 1 || ($status == 4 && get_request_var('status') > 0)) && read_user_setting('monitor_sound') == 'First Orders Suite.mp3') {
|
|
return 'fab fa-first-order fa-spin mon_icon';
|
|
}
|
|
|
|
if ($icon != '' && array_key_exists($icon, $fa_icons)) {
|
|
if (isset($fa_icons[$icon]['class'])) {
|
|
return $fa_icons[$icon]['class'] . ' mon_icon';
|
|
} else {
|
|
return "fa fa-$icon mon_icon";
|
|
}
|
|
} else {
|
|
return 'fa fa-server' . ' mon_icon';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert uptime/fail timestamp into compact human-readable duration text.
|
|
*
|
|
* @param int|string $status_time Timestamp string or SNMP uptime ticks.
|
|
* @param bool $seconds Include seconds in output string.
|
|
*
|
|
* @return string
|
|
*/
|
|
function monitorPrintHostTime(mixed $status_time, bool $seconds = false): string {
|
|
// If the host is down, make a downtime since message
|
|
$dt = '';
|
|
|
|
if (is_numeric($status_time)) {
|
|
$sfd = round($status_time / 100, 0);
|
|
} else {
|
|
$sfd = time() - strtotime($status_time);
|
|
}
|
|
$dt_d = floor($sfd / 86400);
|
|
$dt_h = floor(($sfd - ($dt_d * 86400)) / 3600);
|
|
$dt_m = floor(($sfd - ($dt_d * 86400) - ($dt_h * 3600)) / 60);
|
|
$dt_s = $sfd - ($dt_d * 86400) - ($dt_h * 3600) - ($dt_m * 60);
|
|
|
|
if ($dt_d > 0) {
|
|
$dt .= $dt_d . 'd:' . $dt_h . 'h:' . $dt_m . 'm' . ($seconds ? ':' . $dt_s . 's' : '');
|
|
} elseif ($dt_h > 0) {
|
|
$dt .= $dt_h . 'h:' . $dt_m . 'm' . ($seconds ? ':' . $dt_s . 's' : '');
|
|
} elseif ($dt_m > 0) {
|
|
$dt .= $dt_m . 'm' . ($seconds ? ':' . $dt_s . 's' : '');
|
|
} else {
|
|
$dt .= ($seconds ? $dt_s . 's' : __('Just Up', 'monitor'));
|
|
}
|
|
|
|
return $dt;
|
|
}
|
|
|
|
/**
|
|
* Trim monitor text fields for quote and whitespace artifacts.
|
|
*
|
|
* @param string $string Input string.
|
|
*
|
|
* @return string
|
|
*/
|
|
function monitorTrim(string $string): string {
|
|
return trim($string, "\"'\\ \n\t\r");
|
|
}
|
|
|
|
/**
|
|
* Render wrapper header for default/tile monitor views.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHeaderDefault(): string {
|
|
return "<div class='monitorTable monitor'><div class='monitor_container'>";
|
|
}
|
|
|
|
/**
|
|
* Render wrapper header for names view table.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHeaderNames(): string {
|
|
return "<table class='monitorTable monitor'>";
|
|
}
|
|
|
|
/**
|
|
* Render wrapper header for icon tile view.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHeaderTiles(): string {
|
|
return renderHeaderDefault();
|
|
}
|
|
|
|
/**
|
|
* Render wrapper header for advanced tile view.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHeaderTilesadt(): string {
|
|
return renderHeaderDefault();
|
|
}
|
|
|
|
/**
|
|
* Event key => short label and status CSS class for the Events column pills.
|
|
*
|
|
* @return array
|
|
*/
|
|
function monitorEventMeta(): array {
|
|
return [
|
|
'down' => ['label' => __('Down', 'monitor'), 'class' => 'deviceDown'],
|
|
'recovering' => ['label' => __('Recovering', 'monitor'), 'class' => 'deviceRecovering'],
|
|
'triggered' => ['label' => __('Triggered', 'monitor'), 'class' => 'deviceThreshold'],
|
|
'down_ma' => ['label' => __('Down (M/A)', 'monitor'), 'class' => 'deviceDownMuted'],
|
|
'triggered_ma' => ['label' => __('Triggered (M/A)', 'monitor'), 'class' => 'deviceThresholdMuted'],
|
|
'warn_ping' => ['label' => __('Warn Ping', 'monitor'), 'class' => 'deviceWarning'],
|
|
'alert_ping' => ['label' => __('Alert Ping', 'monitor'), 'class' => 'deviceAlert'],
|
|
'failed_svc' => ['label' => __('Failed Svc', 'monitor'), 'class' => 'deviceServiceCheck'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Resolve every active event on a host for the Events column (a device can have
|
|
* several, e.g. Down and Triggered). Uses the preserved raw availability status
|
|
* plus the triggered-thold, failing-servcheck and session mute sets.
|
|
*
|
|
* @param array $host Host row data.
|
|
*
|
|
* @return array List of active event keys.
|
|
*/
|
|
function monitorHostEvents(array $host): array {
|
|
global $thold_hosts, $servcheck_hosts;
|
|
|
|
$raw = isset($host['raw_status']) ? (int) $host['raw_status'] : (int) $host['status'];
|
|
$muted = isset($_SESSION['monitor_muted_hosts']) && in_array($host['id'], $_SESSION['monitor_muted_hosts'], true);
|
|
$events = [];
|
|
|
|
if ($raw == 1) {
|
|
$events[] = $muted ? 'down_ma' : 'down';
|
|
} elseif ($raw == 2) {
|
|
$events[] = 'recovering';
|
|
}
|
|
|
|
if (isset($thold_hosts) && array_key_exists($host['id'], $thold_hosts)) {
|
|
$events[] = $muted ? 'triggered_ma' : 'triggered';
|
|
}
|
|
|
|
if ($raw == 3) {
|
|
if (!empty($host['monitor_alert']) && $host['cur_time'] > $host['monitor_alert']) {
|
|
$events[] = 'alert_ping';
|
|
} elseif (!empty($host['monitor_warn']) && $host['cur_time'] > $host['monitor_warn']) {
|
|
$events[] = 'warn_ping';
|
|
}
|
|
}
|
|
|
|
// A failing service check is additive and independent of ping availability:
|
|
// monitorEventFilterSql('failed_svc') matches the host regardless of its Up/
|
|
// Down state, so a Down+Failed-Svc host must still get the Failed Svc pill.
|
|
if (isset($servcheck_hosts) && array_key_exists($host['id'], $servcheck_hosts)) {
|
|
$events[] = 'failed_svc';
|
|
}
|
|
|
|
return $events;
|
|
}
|
|
|
|
/**
|
|
* Render header and sortable column bar for list view.
|
|
*
|
|
* @param int $total_rows Total rows matching active filters.
|
|
* @param int $rows Page row limit.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHeaderList(int $total_rows = 0, int $rows = 0): string {
|
|
$display_text = [
|
|
'nosort_actions' => [
|
|
'display' => __('Actions', 'monitor'),
|
|
'align' => 'left',
|
|
'tip' => __('Open the device detail panels', 'monitor')
|
|
],
|
|
'description' => [
|
|
'display' => __('Description', 'monitor'),
|
|
'sort' => 'ASC',
|
|
'align' => 'left',
|
|
'tip' => __('Device description as set on the Devices page', 'monitor')
|
|
],
|
|
'location' => [
|
|
'display' => __('Location', 'monitor'),
|
|
'sort' => 'ASC',
|
|
'align' => 'left',
|
|
'tip' => __('Physical location recorded for the device', 'monitor')
|
|
],
|
|
'site_name' => [
|
|
'display' => __('Site', 'monitor'),
|
|
'sort' => 'ASC',
|
|
'align' => 'left',
|
|
'tip' => __('Site the device is assigned to', 'monitor')
|
|
],
|
|
'id' => [
|
|
'display' => __('ID', 'monitor'),
|
|
'sort' => 'ASC',
|
|
'align' => 'left',
|
|
'tip' => __('Cacti internal device id', 'monitor')
|
|
],
|
|
'monitor_criticality' => [
|
|
'display' => __('Criticality', 'monitor'),
|
|
'sort' => 'ASC',
|
|
'align' => 'left',
|
|
'tip' => __('Monitoring criticality assigned to the device', 'monitor')
|
|
],
|
|
'nosort_events' => [
|
|
'display' => __('Events', 'monitor'),
|
|
'align' => 'center',
|
|
'tip' => __('Active events on the device (Down, Triggered, Warn/Alert Ping, Failed Svc, ...); click a pill to add or remove that event from the Events filter', 'monitor')
|
|
],
|
|
'nosort_tholds' => [
|
|
'display' => __('Thresholds', 'monitor'),
|
|
'align' => 'center',
|
|
'tip' => __('Breached / Triggered / Total thresholds (thold plugin) on the device; click to toggle the Triggered event filter', 'monitor')
|
|
],
|
|
'nosort_servchecks' => [
|
|
'display' => __('Service Checks', 'monitor'),
|
|
'align' => 'center',
|
|
'tip' => __('Failed / Total service checks (servcheck plugin) for the device; click to toggle the Failed Svc event filter', 'monitor')
|
|
],
|
|
'monitor_warn' => [
|
|
'display' => __('Warning', 'monitor'),
|
|
'sort' => 'DESC',
|
|
'align' => 'left',
|
|
'tip' => __('Ping latency Warning / Alert thresholds (ms) configured for this device; a notification is raised when the response time crosses them', 'monitor')
|
|
],
|
|
'monitor_text' => [
|
|
'display' => __('Admin', 'monitor'),
|
|
'sort' => 'ASC',
|
|
'tip' => __('Administrative monitor message shown while the device is Down or Recovering', 'monitor'),
|
|
'align' => 'left'
|
|
],
|
|
'availability' => [
|
|
'display' => __('Availability', 'monitor'),
|
|
'sort' => 'DESC',
|
|
'align' => 'right',
|
|
'tip' => __('Percentage of successful availability checks over the retained history', 'monitor')
|
|
],
|
|
'avg_time' => [
|
|
'display' => __('Averages', 'monitor'),
|
|
'sort' => 'DESC',
|
|
'align' => 'right',
|
|
'tip' => __('Current / average ping response time in milliseconds', 'monitor')
|
|
],
|
|
'instate' => [
|
|
'display' => __('Length in Status', 'monitor'),
|
|
'sort' => 'ASC',
|
|
'align' => 'right',
|
|
'tip' => __('How long the device has been in its current status', 'monitor')
|
|
],
|
|
'status_fail_date' => [
|
|
'display' => __('Last Fail', 'monitor'),
|
|
'sort' => 'DESC',
|
|
'align' => 'right',
|
|
'tip' => __('Date and time the device last failed its availability check', 'monitor')
|
|
],
|
|
];
|
|
|
|
// Hide the Thresholds / Service Checks columns when their plugin is absent.
|
|
if (!api_plugin_is_enabled('thold')) {
|
|
unset($display_text['nosort_tholds']);
|
|
}
|
|
|
|
if (!api_plugin_is_enabled('servcheck') || !api_plugin_user_realm_auth('servcheck_test.php')) {
|
|
unset($display_text['nosort_servchecks']);
|
|
}
|
|
|
|
ob_start();
|
|
|
|
$nav = html_nav_bar('monitor.php?rfilter=' . rawurlencode(get_request_var('rfilter')), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, 12, __('Devices'), 'page', 'main');
|
|
|
|
html_start_box(__('Monitored Devices', 'monitor'), '100%', false, 3, 'center', '');
|
|
|
|
print $nav;
|
|
|
|
html_header_sort($display_text, get_request_var('sort_column'), get_request_var('sort_direction'), false);
|
|
|
|
$output = ob_get_contents();
|
|
|
|
ob_end_clean();
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Indicate whether grouped section headers are suppressed for list view.
|
|
*
|
|
* @return bool
|
|
*/
|
|
function renderSuppressgroupsList(): bool {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Render wrapper footer for default/tile monitor views.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderFooterDefault(): string {
|
|
return '</div></div>';
|
|
}
|
|
|
|
/**
|
|
* Render footer row for names view including trailing empty cells.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderFooterNames(): string {
|
|
$col = 7 - $_SESSION['names'];
|
|
|
|
if ($col == 0) {
|
|
return '</tr></table>';
|
|
} else {
|
|
return '<td colspan="' . $col . '"></td></tr></table>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render wrapper footer for icon tile view.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderFooterTiles(): string {
|
|
return renderFooterDefault();
|
|
}
|
|
|
|
/**
|
|
* Render wrapper footer for advanced tile view.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderFooterTilesadt(): string {
|
|
return renderFooterDefault();
|
|
}
|
|
|
|
/**
|
|
* Render list view footer and bottom pager.
|
|
*
|
|
* @param int $total_rows Total rows matching active filters.
|
|
* @param int $rows Page row limit.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderFooterList(int $total_rows, int $rows): string {
|
|
ob_start();
|
|
|
|
html_end_box(false);
|
|
|
|
if ($total_rows > 0) {
|
|
$nav = html_nav_bar('monitor.php?rfilter=' . rawurlencode(get_request_var('rfilter')), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, 12, __('Devices'), 'page', 'main');
|
|
|
|
print $nav;
|
|
}
|
|
|
|
$output = ob_get_contents();
|
|
|
|
ob_end_clean();
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Render one host row in list view.
|
|
*
|
|
* @param array $host Host row data.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHostList(array $host): string {
|
|
global $criticalities, $iclasses, $thold_counts, $servcheck_counts, $config;
|
|
|
|
if ($host['status'] < 2 || $host['status'] == 5) {
|
|
$dt = get_timeinstate($host);
|
|
} elseif (strtotime($host['status_rec_date']) > 192800) {
|
|
$dt = get_timeinstate($host);
|
|
} else {
|
|
$dt = __('Never', 'monitor');
|
|
}
|
|
|
|
if ($host['status'] < 3 || $host['status'] == 5) {
|
|
$host_admin = $host['monitor_text'];
|
|
} else {
|
|
$host_admin = '';
|
|
}
|
|
|
|
$crit_level = isset($host['monitor_criticality']) ? (int) $host['monitor_criticality'] : 0;
|
|
|
|
if ($crit_level > 0) {
|
|
$host_crit = "<span class='monitorPill monitorCrit monitorCrit$crit_level monitorFilterPill' role='button' tabindex='0' data-filter='crit' data-value='$crit_level' title='" . __esc('Filter by this criticality (click again to clear)', 'monitor') . "'>" . html_escape($criticalities[$crit_level]) . '</span>';
|
|
} else {
|
|
$host_crit = "<span class='monitorPill monitorCrit monitorCrit0 monitorFilterPill' role='button' tabindex='0' data-filter='crit' data-value='0' title='" . __esc('Criticality not set - click to filter unset devices (click again to clear)', 'monitor') . "'>" . __esc('Not Set', 'monitor') . '</span>';
|
|
}
|
|
|
|
// Events column: a short pill for every active event (a device can have
|
|
// several, e.g. Down + Triggered); each pill toggles that event in the
|
|
// multi-select filter. A muted 'None' pill is shown when the device is clean.
|
|
$event_keys = monitorHostEvents($host);
|
|
|
|
if (cacti_sizeof($event_keys)) {
|
|
$meta = monitorEventMeta();
|
|
$pills = [];
|
|
|
|
foreach ($event_keys as $ekey) {
|
|
if (isset($meta[$ekey])) {
|
|
$pills[] = "<span class='monitorPill " . $meta[$ekey]['class'] . " monitorFilterPill' role='button' tabindex='0' data-filter='events' data-value='$ekey' title='" . __esc('Click to add or remove this event from the filter', 'monitor') . "'>" . html_escape($meta[$ekey]['label']) . '</span>';
|
|
}
|
|
}
|
|
|
|
$events_cell = implode(' ', $pills);
|
|
} else {
|
|
$events_cell = "<span class='monitorPill monitorPillMuted'>" . __esc('None', 'monitor') . '</span>';
|
|
}
|
|
|
|
// Thresholds pill: breached / triggered / total (thold plugin); a muted
|
|
// 'Not Set' pill is shown when the device has no thresholds. Clicking toggles
|
|
// the Triggered event.
|
|
if (isset($thold_counts[$host['id']]) && $thold_counts[$host['id']]['total'] > 0) {
|
|
$tc = $thold_counts[$host['id']];
|
|
$tcls = ($tc['triggered'] > 0 || $tc['breached'] > 0) ? 'deviceThreshold' : 'monitorPillMuted';
|
|
$thold_cell = "<span class='monitorPill $tcls monitorFilterPill' role='button' tabindex='0' data-filter='events' data-value='triggered' title='" . __esc('Breached / Triggered / Total thresholds - click to toggle the Triggered event filter', 'monitor') . "'>" . $tc['breached'] . ' / ' . $tc['triggered'] . ' / ' . $tc['total'] . '</span>';
|
|
} else {
|
|
$thold_cell = "<span class='monitorPill monitorPillMuted' title='" . __esc('No thresholds defined for this device', 'monitor') . "'>" . __esc('Not Set', 'monitor') . '</span>';
|
|
}
|
|
|
|
// Service checks pill: failed / total (servcheck plugin); a muted 'Not Set'
|
|
// pill is shown when the device has no service checks. Clicking toggles the
|
|
// Failed Svc event.
|
|
if (isset($servcheck_counts[$host['id']]) && $servcheck_counts[$host['id']]['total'] > 0) {
|
|
$sc = $servcheck_counts[$host['id']];
|
|
$scls = ($sc['failed'] > 0) ? 'deviceServiceCheck' : 'monitorPillMuted';
|
|
$servcheck_cell = "<span class='monitorPill $scls monitorFilterPill' role='button' tabindex='0' data-filter='events' data-value='failed_svc' title='" . __esc('Failed / Total service checks - click to toggle the Failed Svc event filter', 'monitor') . "'>" . $sc['failed'] . ' / ' . $sc['total'] . '</span>';
|
|
} else {
|
|
$servcheck_cell = "<span class='monitorPill monitorPillMuted' title='" . __esc('No service checks defined for this device', 'monitor') . "'>" . __esc('Not Set', 'monitor') . '</span>';
|
|
}
|
|
|
|
if ($host['availability_method'] > 0) {
|
|
$host_avg = __('%d ms', $host['cur_time'], 'monitor') . ' / ' . __('%d ms', $host['avg_time'], 'monitor');
|
|
} else {
|
|
$host_avg = __('N/A', 'monitor');
|
|
}
|
|
|
|
if (isset($host['monitor_warn']) && ($host['monitor_warn'] > 0 || $host['monitor_alert'] > 0)) {
|
|
$host_warn = __('%0.2d ms', $host['monitor_warn'], 'monitor') . ' / ' . __('%0.2d ms', $host['monitor_alert'], 'monitor');
|
|
} else {
|
|
$host_warn = '';
|
|
}
|
|
|
|
if (strtotime($host['status_fail_date']) < 86400) {
|
|
$host['status_fail_date'] = __('Never', 'monitor');
|
|
}
|
|
|
|
$host_datefail = $host['status_fail_date'];
|
|
|
|
ob_start();
|
|
|
|
print "<tr class='tableRow line{$host['id']} selectable'>";
|
|
|
|
$actions = "<a class='monitorActions pic' href='#' data-id='" . $host['id'] . "' title='" . __esc('Device Detail Panels', 'monitor') . "'><i class='fas fa-layer-group'></i></a>";
|
|
|
|
$actions .= "<a class='monitorActions pic' href='" . html_escape($config['url_path'] . 'graph_view.php?action=preview&reset=1&host_id=' . $host['id']) . "' title='" . __esc('View Graphs', 'monitor') . "'><i class='fas fa-chart-line monitorGraph'></i></a>";
|
|
|
|
if (api_plugin_user_realm_auth('host.php')) {
|
|
$actions .= "<a class='monitorActions pic' href='" . html_escape($config['url_path'] . 'host.php?action=edit&id=' . $host['id']) . "' title='" . __esc('Edit Device', 'monitor') . "'><i class='fas fa-pen-square'></i></a>";
|
|
}
|
|
|
|
form_selectable_cell($actions, $host['id'], '1%', 'left');
|
|
form_selectable_cell($host['description'], $host['id'], '', 'left');
|
|
form_selectable_cell($host['location'], $host['id'], '', 'left');
|
|
form_selectable_cell($host['site_name'], $host['id'], '', 'left');
|
|
form_selectable_cell($host['id'], $host['id'], '', 'left');
|
|
form_selectable_cell($host_crit, $host['id'], '', 'left');
|
|
form_selectable_cell($events_cell, $host['id'], '', 'center');
|
|
|
|
if (api_plugin_is_enabled('thold')) {
|
|
form_selectable_cell($thold_cell, $host['id'], '', 'center');
|
|
}
|
|
|
|
if (api_plugin_is_enabled('servcheck') && api_plugin_user_realm_auth('servcheck_test.php')) {
|
|
form_selectable_cell($servcheck_cell, $host['id'], '', 'center');
|
|
}
|
|
|
|
form_selectable_cell($host_warn, $host['id'], '', 'left');
|
|
form_selectable_cell($host_admin, $host['id'], '', 'white-space:pre-wrap;text-align:left');
|
|
form_selectable_cell(round($host['availability'], 2) . ' %', $host['id'], '', 'right');
|
|
form_selectable_cell($host_avg, $host['id'], '', 'right');
|
|
form_selectable_cell($dt, $host['id'], '', 'right');
|
|
form_selectable_cell($host_datefail, $host['id'], '', 'right');
|
|
|
|
form_end_row();
|
|
|
|
$result = ob_get_contents();
|
|
|
|
ob_end_clean();
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Render one host cell in names view grid.
|
|
*
|
|
* @param array $host Host row data.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHostNames(array $host): string {
|
|
$fclass = get_request_var('size');
|
|
|
|
$result = '';
|
|
|
|
$maxlen = getMonitorTrimLength(100);
|
|
|
|
if ($_SESSION['names'] == 0) {
|
|
$result .= '<tr>';
|
|
}
|
|
|
|
if ($host['status'] <= 2 || $host['status'] == 5) {
|
|
$result .= "<td class='{$fclass}_names flash'><a class='hyperLink' href='" . html_escape($host['anchor']) . "'><span class='{$fclass} deviceDown '>" . title_trim(html_escape($host['description']), $maxlen) . '</span></a></td>';
|
|
} else {
|
|
$result .= "<td class='{$fclass}_names'><a class='hyperLink' href='" . html_escape($host['anchor']) . "'><span class='{$fclass}'>" . title_trim(html_escape($host['description']), $maxlen) . '</span></a></td>';
|
|
}
|
|
|
|
$_SESSION['names']++;
|
|
|
|
if ($_SESSION['names'] > 7) {
|
|
$result .= '</tr>';
|
|
$_SESSION['names'] = 0;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Render one host icon tile.
|
|
*
|
|
* @param array $host Host row data.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHostTiles(array $host): string {
|
|
$class = getStatusIcon($host['status'], $host['monitor_icon']);
|
|
$fclass = get_request_var('size');
|
|
|
|
return "<div class='{$fclass}_tiles monitor_device_frame'><a class='pic hyperLink textSubHeaderDark' href='" . html_escape($host['anchor']) . "'><i id='" . $host['id'] . "' class='$class " . $host['iclass'] . "'></i></a></div>";
|
|
}
|
|
|
|
/**
|
|
* Render one advanced host tile including time-in-state/uptime text.
|
|
*
|
|
* @param array $host Host row data.
|
|
*
|
|
* @return string
|
|
*/
|
|
function renderHostTilesadt(array $host): string {
|
|
$tis = '';
|
|
|
|
$class = getStatusIcon($host['status'], $host['monitor_icon']);
|
|
$fclass = get_request_var('size');
|
|
$name = title_trim(html_escape($host['description']), 40);
|
|
|
|
if ($host['status'] < 2 || $host['status'] == 5) {
|
|
$tis = get_timeinstate($host);
|
|
|
|
return "<div class='{$fclass}_tilesadt monitor_device_frame'><a class='pic hyperLink textSubHeaderDark' href='" . html_escape($host['anchor']) . "'><i id='" . $host['id'] . "' class='$class " . $host['iclass'] . "'></i><br><span class='{$fclass}_title'>$name</span><br><span class='monitor_device_{$fclass} deviceDown monitor_uptime'>$tis</span></a></div>";
|
|
} else {
|
|
$tis = get_uptime($host);
|
|
|
|
return "<div class='{$fclass}_tilesadt monitor_device_frame'><a class='pic hyperLink textSubHeaderDark' href='" . html_escape($host['anchor']) . "'><i id='" . $host['id'] . "' class='$class " . $host['iclass'] . "'></i><br><span class='{$fclass}_title'>$name</span><br><span class='monitor_device_{$fclass} deviceUp monitor_uptime'>$tis</span></a></div>";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply monitor trim setting to a computed source field length.
|
|
*
|
|
* @param int $fieldlen Initial field length.
|
|
*
|
|
* @return int
|
|
*/
|
|
function getMonitorTrimLength(int $fieldlen): int {
|
|
global $maxchars;
|
|
|
|
if (get_request_var('view') == 'default' || get_request_var('view') == 'names') {
|
|
$maxlen = $maxchars;
|
|
|
|
if (get_request_var('trim') < 0) {
|
|
$maxlen = 4000;
|
|
} elseif (get_request_var('trim') > 0) {
|
|
$maxlen = get_request_var('trim');
|
|
}
|
|
|
|
if ($fieldlen > $maxlen) {
|
|
$fieldlen = $maxlen;
|
|
}
|
|
}
|
|
|
|
return $fieldlen;
|
|
}
|