From 2f815132826fb7b05ef9e9ef70482a20e4d39380 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 8 Oct 2026 16:06:20 -0400 Subject: [PATCH 1/2] Add clickable status pills + zebra rows to Device Status and Threshold Log pages Mirror the Thresholds-page pill treatment on the two remaining thold_graph.php sub-views: the Device Status (hoststat) list now renders each device's status as a clickable pill and zebra-stripes its rows instead of colouring the whole row, and the Threshold Log gains a clickable Status pill column right after Type (rows zebra-striped). Clicking a pill sets the page's Status filter and auto-applies. Regenerated cacti.pot for the new Log Status header. --- CHANGELOG.md | 3 +- locales/po/cacti.pot | 2 +- thold_graph.php | 96 +++++++++++++++++++++++++++++--------------- 3 files changed, 66 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdfe6a52..cb0b2423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 * 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 diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 071e1b2f..d0a71f62 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -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 \n" "Language-Team: Cacti Developers \n" diff --git a/thold_graph.php b/thold_graph.php index 0417267f..9709498d 100644 --- a/thold_graph.php +++ b/thold_graph.php @@ -880,51 +880,55 @@ function tholds() { } /** - * Prints the opening `` 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 ""; + // Each state maps to its matching Device Status filter (host_status) value. + $state_map = [ + 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 "" . html_escape(get_uncolored_device_status($host)) . ''; } /** * 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 @@ -1195,7 +1199,7 @@ function hosts() { } if ($host['availability_method'] != 0) { - form_host_status_row_color($host); + print ""; $actions_url = ''; @@ -1212,7 +1216,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'); @@ -1221,7 +1225,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 ""; + print ""; $actions_url = ''; @@ -1238,7 +1242,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("" . __esc('Not Monitored', 'thold') . '', $host['id'], '', 'center'); form_selectable_cell(__('N/A', 'thold'), $host['id'], '', 'right'); form_selectable_cell($uptime, $host['id'], '', 'right'); @@ -1420,6 +1424,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(); }); @@ -1677,7 +1691,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; @@ -1699,6 +1713,11 @@ function thold_show_log() { 'sort' => 'DESC', 'align' => 'left' ], + 'status' => [ + 'display' => __('Status', 'thold'), + 'sort' => '', + 'align' => 'left' + ], 'description' => [ 'display' => __('Event Description', 'thold'), 'sort' => 'ASC', @@ -1748,18 +1767,19 @@ function thold_show_log() { $baseu = 1024; } - print ""; + print ""; 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("" . html_escape($thold_log_states[$l['status']]['display_short']) . '', $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 '' . __('No Threshold Logs Found', 'thold') . ''; + print '' . __('No Threshold Logs Found', 'thold') . ''; } html_end_box(false); @@ -1968,6 +1988,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(); }); From 4457c400f6abc827a9866d5819f3289c76a06857 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 8 Oct 2026 20:01:53 -0400 Subject: [PATCH 2/2] Make Device Status pill filters match displayed states Address review feedback on PR #853: the clicked status pills did not consistently filter to the state shown on the device row. - Map the Error pill to an exact Error filter (host_status=4) instead of the aggregate Not Up (-4), and add an Error option to the Status dropdown. - Rework the host_status WHERE clause so each state reuses the same failure-count override classification as thold_device_status_pill(): the override now uses >= (matching the pill, was >) and only reclassifies hosts to Down, so the Down filter includes overridden hosts and the Up/Unknown/Error filters exclude them. --- CHANGELOG.md | 2 +- thold_graph.php | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb0b2423..d0dbb6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ --- 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 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 +* 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 diff --git a/thold_graph.php b/thold_graph.php index 9709498d..af2090c7 100644 --- a/thold_graph.php +++ b/thold_graph.php @@ -916,7 +916,7 @@ function thold_device_status_pill(&$host) { HOST_RECOVERING => '2', HOST_UP => '3', HOST_UNKNOWN => '0', - HOST_ERROR => '-4', + HOST_ERROR => '4', 'disabled' => '-2' ]; @@ -1069,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') { @@ -1337,6 +1349,7 @@ function form_host_filter() { +