From da8e3df5c582122fb9f41031ed6289ba1c2c4638 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Wed, 7 Oct 2026 10:50:17 -0400 Subject: [PATCH 1/3] syslog: equal-width severity-legend chips via CSS grid + longest-label seed Brings the System Logs and Alert Logs severity legends in line with the thold/monitor/servcheck/mactrack legends. syslog_syslog_legend() and syslog_log_legend() now emit --syslog-chip-min (calc(longest-label ch + 1.5rem)), and .syslogLegend is a grid (repeat(auto-fit, minmax(var(--syslog-chip-min), 1fr))) across all nine theme stylesheets instead of flex: 1 1 0 (which only equalised chips within a wrapped line). Tests assert the exact chip-min value. --- CHANGELOG.md | 1 + css/classic.css | 5 ++-- css/dark.css | 5 ++-- css/deepness.css | 5 ++-- css/legend.css | 5 ++-- css/midwinter.css | 5 ++-- css/modern.css | 5 ++-- css/paper-plane.css | 5 ++-- css/paw.css | 5 ++-- css/sunrise.css | 5 ++-- includes/functions.php | 52 ++++++++++++++++++++++++--------- tests/Unit/SyslogLegendTest.php | 27 +++++++++++++---- 12 files changed, 79 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df657411..cc007609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ --- develop --- +* dev: Size the severity-legend chips to the longest label and lay them out as a CSS grid so every chip stays equal width as the legend wraps (matching the thold/monitor/servcheck/mactrack legends) * ci: Drop Ruby from the CodeQL language matrix; the plugin ships no Ruby source, so the Ruby database build failed with "no source code seen" * dev: Move the shared function and database libraries to includes/ and point the entry-point includes, coverage source, and tests at the new path * feature: Restyle the System Logs and Alert Logs severity legends as rounded, evenly spaced, solid-colour chips that line up with the Thold status legends, with theme-appropriate backgrounds loaded from a per-theme css/.css file (falling back to a theme-neutral css/legend.css) diff --git a/css/classic.css b/css/classic.css index 7eee6b3c..42c80443 100644 --- a/css/classic.css +++ b/css/classic.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the classic (light surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/dark.css b/css/dark.css index e8692ed9..d43b99f1 100644 --- a/css/dark.css +++ b/css/dark.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the dark (dark surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/deepness.css b/css/deepness.css index e8692ed9..d43b99f1 100644 --- a/css/deepness.css +++ b/css/deepness.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the dark (dark surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/legend.css b/css/legend.css index a627d08f..787bdc29 100644 --- a/css/legend.css +++ b/css/legend.css @@ -24,14 +24,13 @@ /* Theme-neutral Syslog status-legend palette; the fallback loaded by syslog_include_js() when the active theme ships no dedicated css/.css file. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/midwinter.css b/css/midwinter.css index 1febac31..c06840dc 100644 --- a/css/midwinter.css +++ b/css/midwinter.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the midwinter (dark surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/modern.css b/css/modern.css index 3ded6964..31441cde 100644 --- a/css/modern.css +++ b/css/modern.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the modern (light surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/paper-plane.css b/css/paper-plane.css index 089d85ad..7fcb65ae 100644 --- a/css/paper-plane.css +++ b/css/paper-plane.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the paper-plane (dark surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/paw.css b/css/paw.css index 07d1b4c3..12bd0c4a 100644 --- a/css/paw.css +++ b/css/paw.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the paw (light surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/css/sunrise.css b/css/sunrise.css index 00432e34..670a93e0 100644 --- a/css/sunrise.css +++ b/css/sunrise.css @@ -24,14 +24,13 @@ /* Syslog status-legend palette for the sunrise (dark surface) theme. */ .syslogLegend { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--syslog-chip-min, 8ch), 1fr)); gap: 4px; width: 100%; } .syslogLegend .syslogLegendItem { - flex: 1 1 0; text-align: center; white-space: nowrap; padding: 4px 6px; diff --git a/includes/functions.php b/includes/functions.php index 0cb4d129..a1f80fd2 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -747,16 +747,29 @@ function syslog_include_js(): void { function syslog_syslog_legend(): void { html_start_box('', '100%', '', '3', 'center', ''); + $items = array( + 'logEmergency' => __('Emergency', 'syslog'), + 'logCritical' => __('Critical', 'syslog'), + 'logAlert' => __('Alert', 'syslog'), + 'logError' => __('Error', 'syslog'), + 'logWarning' => __('Warning', 'syslog'), + 'logNotice' => __('Notice', 'syslog'), + 'logInfo' => __('Info', 'syslog'), + 'logDebug' => __('Debug', 'syslog'), + ); + + $chip_min = 0; + foreach ($items as $label) { + $chip_min = max($chip_min, mb_strlen($label)); + } + print ''; - print '
'; - print '
' . __('Emergency', 'syslog') . '
'; - print '
' . __('Critical', 'syslog') . '
'; - print '
' . __('Alert', 'syslog') . '
'; - print '
' . __('Error', 'syslog') . '
'; - print '
' . __('Warning', 'syslog') . '
'; - print '
' . __('Notice', 'syslog') . '
'; - print '
' . __('Info', 'syslog') . '
'; - print '
' . __('Debug', 'syslog') . '
'; + print '
'; + + foreach ($items as $class => $label) { + print '
' . $label . '
'; + } + print '
'; print ''; @@ -774,11 +787,24 @@ function syslog_syslog_legend(): void { function syslog_log_legend(): void { html_start_box('', '100%', '', '3', 'center', ''); + $items = array( + 'logAlert' => __('Alert', 'syslog'), + 'logWarning' => __('Warning', 'syslog'), + 'logInfo' => __('Informational', 'syslog'), + ); + + $chip_min = 0; + foreach ($items as $label) { + $chip_min = max($chip_min, mb_strlen($label)); + } + print ''; - print '
'; - print '
' . __('Alert', 'syslog') . '
'; - print '
' . __('Warning', 'syslog') . '
'; - print '
' . __('Informational', 'syslog') . '
'; + print '
'; + + foreach ($items as $class => $label) { + print '
' . $label . '
'; + } + print '
'; print ''; diff --git a/tests/Unit/SyslogLegendTest.php b/tests/Unit/SyslogLegendTest.php index 91fdef98..16d9312e 100644 --- a/tests/Unit/SyslogLegendTest.php +++ b/tests/Unit/SyslogLegendTest.php @@ -41,12 +41,20 @@ $output = ob_get_clean(); expect($output)->toContain(''); - expect($output)->toContain('
'); - expect(substr_count($output, 'syslogLegendItem'))->toBe(8); - foreach (['logEmergency' => 'Emergency', 'logCritical' => 'Critical', 'logAlert' => 'Alert', + $items = ['logEmergency' => 'Emergency', 'logCritical' => 'Critical', 'logAlert' => 'Alert', 'logError' => 'Error', 'logWarning' => 'Warning', 'logNotice' => 'Notice', - 'logInfo' => 'Info', 'logDebug' => 'Debug'] as $class => $label) { + 'logInfo' => 'Info', 'logDebug' => 'Debug']; + + $expected = 0; + foreach ($items as $label) { + $expected = max($expected, mb_strlen($label)); + } + + expect($output)->toContain('
'); + expect(substr_count($output, 'syslogLegendItem'))->toBe(8); + + foreach ($items as $class => $label) { expect($output)->toContain('
' . $label . '
'); } }); @@ -58,10 +66,17 @@ syslog_log_legend(); $output = ob_get_clean(); - expect($output)->toContain('
'); + $items = ['logAlert' => 'Alert', 'logWarning' => 'Warning', 'logInfo' => 'Informational']; + + $expected = 0; + foreach ($items as $label) { + $expected = max($expected, mb_strlen($label)); + } + + expect($output)->toContain('
'); expect(substr_count($output, 'syslogLegendItem'))->toBe(3); - foreach (['logAlert' => 'Alert', 'logWarning' => 'Warning', 'logInfo' => 'Informational'] as $class => $label) { + foreach ($items as $class => $label) { expect($output)->toContain('
' . $label . '
'); } }); From d53f9c3d245ca300da88a4cdd7a30135ee8e36ca Mon Sep 17 00:00:00 2001 From: TheWitness Date: Wed, 7 Oct 2026 11:01:03 -0400 Subject: [PATCH 2/3] syslog: short-array syntax for legend items; test sizing follows translated labels Review follow-up (#390): use [] short-array syntax for the new legend item collections to match the rest of the file; add a test that overrides __() so a short English label ('Error') has the longest translation, proving --syslog-chip-min is sized from the emitted (translated) output rather than the English source. --- includes/functions.php | 8 ++++---- tests/Unit/SyslogLegendTest.php | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index a1f80fd2..be781ae6 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -747,7 +747,7 @@ function syslog_include_js(): void { function syslog_syslog_legend(): void { html_start_box('', '100%', '', '3', 'center', ''); - $items = array( + $items = [ 'logEmergency' => __('Emergency', 'syslog'), 'logCritical' => __('Critical', 'syslog'), 'logAlert' => __('Alert', 'syslog'), @@ -756,7 +756,7 @@ function syslog_syslog_legend(): void { 'logNotice' => __('Notice', 'syslog'), 'logInfo' => __('Info', 'syslog'), 'logDebug' => __('Debug', 'syslog'), - ); + ]; $chip_min = 0; foreach ($items as $label) { @@ -787,11 +787,11 @@ function syslog_syslog_legend(): void { function syslog_log_legend(): void { html_start_box('', '100%', '', '3', 'center', ''); - $items = array( + $items = [ 'logAlert' => __('Alert', 'syslog'), 'logWarning' => __('Warning', 'syslog'), 'logInfo' => __('Informational', 'syslog'), - ); + ]; $chip_min = 0; foreach ($items as $label) { diff --git a/tests/Unit/SyslogLegendTest.php b/tests/Unit/SyslogLegendTest.php index 16d9312e..c6afa5fb 100644 --- a/tests/Unit/SyslogLegendTest.php +++ b/tests/Unit/SyslogLegendTest.php @@ -81,6 +81,25 @@ } }); +it('sizes the chips from the translated labels, not the English source', function () { + syslog_load_plugin_source('includes/functions.php'); + + $long = 'A very long translated error label'; + + // 'Error' is short in English; its translation is the longest label, so the + // chip minimum must follow the translated (emitted) output, not the source. + test_override('__', function ($text) use ($long) { + return $text === 'Error' ? $long : $text; + }); + + ob_start(); + syslog_syslog_legend(); + $output = ob_get_clean(); + + expect($output)->toContain('--syslog-chip-min: calc(' . mb_strlen($long) . 'ch + 1.5rem)'); + expect($output)->toContain('
' . $long . '
'); +}); + it('links the theme legend stylesheet when the theme ships one', function () { syslog_load_plugin_source('includes/functions.php'); From 6579c01c8bbd10eb3508c005697da3e683042354 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Wed, 7 Oct 2026 12:27:53 -0400 Subject: [PATCH 3/3] syslog: escape status-legend chip labels at output Defence-in-depth: wrap the legend chip display text in html_escape() so markup or HTML entities in a translated label cannot alter the legend DOM. Applied consistently across the thold/monitor/servcheck/mactrack/syslog fleet legends. The chip-min sizing still measures the unescaped label length, so layout is unchanged. Resolves the review note on #390. --- includes/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index be781ae6..c8415af6 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -767,7 +767,7 @@ function syslog_syslog_legend(): void { print '
'; foreach ($items as $class => $label) { - print '
' . $label . '
'; + print '
' . html_escape($label) . '
'; } print '
'; @@ -802,7 +802,7 @@ function syslog_log_legend(): void { print '
'; foreach ($items as $class => $label) { - print '
' . $label . '
'; + print '
' . html_escape($label) . '
'; } print '
';