Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

--- develop ---
* bug: Pass a boolean (not the integer `1`) to `api_plugin_register_hook()`'s `$enable` argument so the hook registration stays type-correct under Cacti 1.3's strict `bool $enable` type-hint (the int only coerces while this file lacks `declare(strict_types=1)`)
* feature: Show each threshold's status as a sortable, clickable pill in a new Status column (right of Name) on the Thresholds and Device Status pages instead of colouring the whole row; rows now zebra-stripe, clicking a pill filters the list to that status, and the Status filter gains per-severity options
* feature: Show each threshold's status as a sortable, clickable pill in a new Status column (right of Name) on the Thresholds page instead of colouring the whole row; rows now zebra-stripe, clicking a pill filters the list to that status, and the Status filter gains per-severity options
* feature: Extend the clickable status pills to the Device Status and Threshold Log pages (thold_graph.php) - device rows now show a status pill and zebra-stripe instead of full-row colouring, and the Threshold Log gains a Status pill column right after Type; clicking a pill filters that list to the chosen status, the Device Status filter gains an exact Error option, and each status filter now classifies the per-host failure-count override the same way as the pill so a clicked pill returns exactly the rows showing that state
* security: Replace the confirmation pages' inline `onClick='cactiReturnTo()'` Cancel buttons with the CSP-safe `cactiReturnTo` class so they no longer trip Cacti's Content-Security-Policy `script-src-attr` directive
* security: Move the filter controls' inline `onChange`/`onClick` handlers into jQuery `ready()` event bindings on the threshold, notification list, notification queue, template and device/log status pages so they no longer trip Cacti's Content-Security-Policy `script-src-attr` directive
* dev: Remove the inert COMPOSER_ROOT_VERSION env from the Pest CI step
Expand Down
2 changes: 1 addition & 1 deletion locales/po/cacti.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Cacti \n"
"Report-Msgid-Bugs-To: developers@cacti.net\n"
"POT-Creation-Date: 2026-10-08 14:32-0400\n"
"POT-Creation-Date: 2026-10-08 16:02-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Cacti Developers <developers@cacti.net>\n"
"Language-Team: Cacti Developers <developers@cacti.net>\n"
Expand Down
115 changes: 79 additions & 36 deletions thold_graph.php
Original file line number Diff line number Diff line change
Expand Up @@ -880,51 +880,55 @@ function tholds() {
}

/**
* Prints the opening `<tr>` tag for a device-status-list row, using a
* Builds a clickable status "pill" for a device-status-list row, using a
* CSS class derived from the host's disabled flag, current status, and
* (when a per-host failure-count override is set) whether its
* consecutive down-event count has reached that threshold.
* consecutive down-event count has reached that threshold. The pill's
* data-state maps to the Device Status filter so clicking it filters the
* list to that status.
*
* @param array &$host The host record to determine the row color/class
* for.
* @param array &$host The host record to build the status pill for.
*
* @return string The CSS class name used for the row.
* @return string The status pill markup.
*
* @global array $thold_host_states Map of status/state key => display
* info (including 'class') used to
* determine the row's CSS class.
* info (including 'class') used to pick
* the pill's colour class.
*/
function form_host_status_row_color(&$host) {
function thold_device_status_pill(&$host) {
global $thold_host_states;

$disabled = $host['disabled'];
$status = $host['status'];
$id = $host['id'];
$status = $host['status'];

// Determine the color to use
if ($disabled) {
$class = $thold_host_states['disabled']['class'];
if ($host['disabled']) {
$key = 'disabled';
} elseif ($host['thold_failure_count'] > 0 && $status != HOST_RECOVERING && $host['status_event_count'] >= $host['thold_failure_count']) {
$key = HOST_DOWN;
} elseif (isset($thold_host_states[$status])) {
$key = $status;
} else {
if ($host['thold_failure_count'] > 0 && $host['status'] != HOST_RECOVERING) {
if ($host['status_event_count'] >= $host['thold_failure_count']) {
$class = $thold_host_states['1']['class'];
} else {
$class = $thold_host_states[$status]['class'];
}
} else {
$class = $thold_host_states[$status]['class'];
}
$key = HOST_UNKNOWN;
}

print "<tr class='tableRow selectable $class' id='line" . $id . "'>";
// Each state maps to its matching Device Status filter (host_status) value.
$state_map = [
Comment thread
TheWitness marked this conversation as resolved.
HOST_DOWN => '1',
HOST_RECOVERING => '2',
HOST_UP => '3',
HOST_UNKNOWN => '0',
HOST_ERROR => '4',
'disabled' => '-2'
];

$state = isset($state_map[$key]) ? $state_map[$key] : '-1';

return $class;
return "<span class='thold-pill " . $thold_host_states[$key]['class'] . "' data-state='" . $state . "' tabindex='0' role='button'>" . html_escape(get_uncolored_device_status($host)) . '</span>';
}

/**
* Determines a human-readable status label for a host (Disabled, Down,
* Recovering, Up, Error, or Unknown), applying the same per-host
* failure-count override logic as form_host_status_row_color() but
* failure-count override logic as thold_device_status_pill() but
* without any HTML/color output.
*
* @param array &$host The host record to determine the status label
Expand Down Expand Up @@ -1065,10 +1069,22 @@ function hosts() {
$sql_where .= ($sql_where == '' ? '(' : ' AND ') . "(h.status != '3' OR h.disabled = 'on')";
} elseif (get_request_var('host_status') == '-5') {
$sql_where .= ($sql_where == '' ? '(' : ' AND ') . '(h.availability_method = 0)';
} elseif (get_request_var('host_status') == '3') {
$sql_where .= ($sql_where == '' ? '(' : ' AND ') . "(h.availability_method != 0 AND h.status = 3 AND h.disabled = '')";
} else {
$sql_where .= ($sql_where == '' ? '(' : ' AND ') . '((h.status = ' . get_request_var('host_status') . " OR h.thold_failure_count > 0 AND h.status_event_count > h.thold_failure_count) AND h.disabled = '')";
// Match the exact state-classification rules used by thold_device_status_pill():
// a per-host failure-count override (status_event_count >= thold_failure_count, excluding
// Recovering) reclassifies the device as Down, so the Down filter includes those hosts and
// the Up/Unknown/Error filters exclude them.
$override_down = 'h.thold_failure_count > 0 AND h.status != ' . HOST_RECOVERING . ' AND h.status_event_count >= h.thold_failure_count';

if (get_request_var('host_status') == HOST_DOWN) {
$status_clause = '(h.status = ' . HOST_DOWN . " OR ($override_down))";
} elseif (get_request_var('host_status') == HOST_RECOVERING) {
$status_clause = 'h.status = ' . HOST_RECOVERING;
} else {
$status_clause = 'h.status = ' . get_request_var('host_status') . " AND NOT ($override_down)";
}

$sql_where .= ($sql_where == '' ? '(' : ' AND ') . "(h.availability_method != 0 AND h.disabled = '' AND $status_clause)";
}

if (get_request_var('host_template_id') == '-1') {
Expand Down Expand Up @@ -1195,7 +1211,7 @@ function hosts() {
}

if ($host['availability_method'] != 0) {
form_host_status_row_color($host);
print "<tr class='tableRow selectable' id='line" . $host['id'] . "'>";

$actions_url = '';

Expand All @@ -1212,7 +1228,7 @@ function hosts() {
form_selectable_cell(number_format_i18n($host['graphs'], -1), $host['id'], '', 'right');
form_selectable_cell(number_format_i18n($host['data_sources'], -1), $host['id'], '', 'right');

form_selectable_cell(get_uncolored_device_status($host), $host['id'], '', 'center');
form_selectable_cell(thold_device_status_pill($host), $host['id'], '', 'center');

form_selectable_cell(get_timeinstate($host), $host['id'], '', 'right');
form_selectable_cell($uptime, $host['id'], '', 'right');
Expand All @@ -1221,7 +1237,7 @@ function hosts() {
form_selectable_cell(number_format_i18n(($host['avg_time']), 2), $host['id'], '', 'right');
form_selectable_cell(number_format_i18n($host['availability'], 2), $host['id'], '', 'right');
} else {
print "<tr class='selectable deviceNotMonFull' id='line" . $host['id'] . "'>";
print "<tr class='tableRow selectable' id='line" . $host['id'] . "'>";

$actions_url = '';

Expand All @@ -1238,7 +1254,7 @@ function hosts() {
form_selectable_cell(number_format_i18n($host['graphs'], -1), $host['id'], '', 'right');
form_selectable_cell(number_format_i18n($host['data_sources'], -1), $host['id'], '', 'right');

form_selectable_cell(__('Not Monitored', 'thold'), $host['id'], '', 'center');
form_selectable_cell("<span class='thold-pill deviceNotMonFull' data-state='-5' tabindex='0' role='button'>" . __esc('Not Monitored', 'thold') . '</span>', $host['id'], '', 'center');

form_selectable_cell(__('N/A', 'thold'), $host['id'], '', 'right');
form_selectable_cell($uptime, $host['id'], '', 'right');
Expand Down Expand Up @@ -1333,6 +1349,7 @@ function form_host_filter() {
<option value='1'<?php if (get_request_var('host_status') == '1') {?> selected<?php }?>><?php print __('Down', 'thold'); ?></option>
<option value='2'<?php if (get_request_var('host_status') == '2') {?> selected<?php }?>><?php print __('Recovering', 'thold'); ?></option>
<option value='0'<?php if (get_request_var('host_status') == '0') {?> selected<?php }?>><?php print __('Unknown', 'thold'); ?></option>
<option value='4'<?php if (get_request_var('host_status') == '4') {?> selected<?php }?>><?php print __('Error', 'thold'); ?></option>
</select>
</td>
<td>
Expand Down Expand Up @@ -1420,6 +1437,16 @@ function clearFilter() {
applyFilter();
});

$('.thold-pill').on('click keydown', function(e) {
if (e.type === 'keydown' && e.which !== 13 && e.which !== 32) {
return;
}

e.preventDefault();
$('#host_status').val($(this).attr('data-state'));
applyFilter();
});

$('#clear').click(function() {
clearFilter();
});
Expand Down Expand Up @@ -1677,7 +1704,7 @@ function thold_show_log() {

$logs = get_allowed_threshold_logs($sql_where, $sql_order, $sql_limit, $total_rows);

$nav = html_nav_bar('thold_graph.php?action=log', MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, 8, __('Log Entries', 'thold'), 'page', 'main');
$nav = html_nav_bar('thold_graph.php?action=log', MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, 9, __('Log Entries', 'thold'), 'page', 'main');

print $nav;

Expand All @@ -1699,6 +1726,11 @@ function thold_show_log() {
'sort' => 'DESC',
'align' => 'left'
],
'status' => [
'display' => __('Status', 'thold'),
'sort' => '',
'align' => 'left'
],
'description' => [
'display' => __('Event Description', 'thold'),
'sort' => 'ASC',
Expand Down Expand Up @@ -1748,18 +1780,19 @@ function thold_show_log() {
$baseu = 1024;
}

print "<tr class='tableRow selectable " . $thold_log_states[$l['status']]['class'] . "' id='line" . $l['id'] . "'>";
print "<tr class='tableRow selectable' id='line" . $l['id'] . "'>";

form_selectable_cell($l['hdescription'], $l['id'], '', 'left');
form_selectable_cell(date('Y-m-d H:i:s', $l['time']), $l['id'], '', 'left');
form_selectable_cell($thold_types[$l['type']], $l['id'], '', 'left');
form_selectable_cell("<span class='thold-pill " . $thold_log_states[$l['status']]['class'] . "' data-state='" . $l['status'] . "' tabindex='0' role='button'>" . html_escape($thold_log_states[$l['status']]['display_short']) . '</span>', $l['id'], '', 'left');
form_selectable_cell((strlen($l['description']) ? filter_value($l['description'], get_request_var('rfilter')) : __('Restoral Event', 'thold')), $l['id'], '', 'left tholdLog');
form_selectable_cell($l['threshold_value'] != '' ? thold_format_number($l['threshold_value'], $decimals, $baseu, $suffix, $show_units, $units_suffix) : __('N/A', 'thold'), $l['id'], '', 'right');
form_selectable_cell($l['current'] != '' ? thold_format_number($l['current'], $decimals, $baseu, $suffix, $show_units, $units_suffix) : __('N/A', 'thold'), $l['id'], '', 'right');
form_end_row();
}
} else {
print '<tr class="tableRow"><td class="center" colspan="8">' . __('No Threshold Logs Found', 'thold') . '</td></tr>';
print '<tr class="tableRow"><td class="center" colspan="9">' . __('No Threshold Logs Found', 'thold') . '</td></tr>';
}

html_end_box(false);
Expand Down Expand Up @@ -1968,6 +2001,16 @@ function exportLog() {
applyFilter();
});

$('.thold-pill').on('click keydown', function(e) {
if (e.type === 'keydown' && e.which !== 13 && e.which !== 32) {
return;
}

e.preventDefault();
$('#status').val($(this).attr('data-state'));
applyFilter();
});

$('#clear').click(function() {
clearFilter();
});
Expand Down
Loading