From 1dbbef8fd4cb8878b4c4e9c3dd50847d132b3d67 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Sat, 10 Oct 2026 08:56:36 -0400 Subject: [PATCH 1/3] provisdes a default dashboard during setup --- includes/schema.php | 1 + js/dashboard.js | 5 +- lib/syslog_dashboard.php | 12 +++- setup.php | 97 ++++++++++++++++++++++++----- tests/Unit/DashboardDefaultTest.php | 54 ++++++++++++++++ 5 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 tests/Unit/DashboardDefaultTest.php diff --git a/includes/schema.php b/includes/schema.php index 9ab9c562..0043fb6f 100644 --- a/includes/schema.php +++ b/includes/schema.php @@ -686,6 +686,7 @@ function syslog_setup_table_new(array $options): void { ROW_FORMAT=Dynamic"); syslog_ensure_share_tables(); + syslog_dashboard_seed_default(); if (!isset($settings['syslog'])) { syslog_config_settings(); diff --git a/js/dashboard.js b/js/dashboard.js index a35e9439..a5ec075d 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -254,7 +254,10 @@ function syslogDashboardRenderChart(panel, data) { return; } - if (status) status.hidden = true; + if (status) { + status.hidden = false; + status.textContent = syslogDashboard.text.totalMessages + ': ' + Number(data.total).toLocaleString(); + } var config; diff --git a/lib/syslog_dashboard.php b/lib/syslog_dashboard.php index bacbf962..b273c5b9 100644 --- a/lib/syslog_dashboard.php +++ b/lib/syslog_dashboard.php @@ -1264,7 +1264,7 @@ function syslog_dashboard(): void { $dashboard_id = 0; } - // Never auto-select a shared dashboard; only an owned one opens by default. + // Prefer an owned dashboard, then the built-in shared default. if ($dashboard_id === 0 && cacti_sizeof($dashboards)) { $username = syslog_dashboard_username(); @@ -1276,6 +1276,15 @@ function syslog_dashboard(): void { } } + if ($dashboard_id === 0) { + foreach ($dashboards as $dashboard) { + if ($dashboard['user'] === '' && $dashboard['is_global'] === 'on' && $dashboard['name'] === 'default') { + $dashboard_id = (int) $dashboard['id']; + break; + } + } + } + $selected = syslog_dashboard_load($dashboard_id); // Match the server-side write permission of every dashboard endpoint. @@ -1354,6 +1363,7 @@ function syslog_dashboard(): void { canShare: , shared: , text: { + totalMessages: , libraryUnavailable: , editPanel: , newPanel: , diff --git a/setup.php b/setup.php index 262ed712..d3d923d9 100644 --- a/setup.php +++ b/setup.php @@ -729,6 +729,7 @@ function syslog_check_upgrade(): void { } syslog_ensure_share_tables(); + syslog_dashboard_seed_default(); // don't let this script timeout ini_set('max_execution_time', 0); @@ -998,6 +999,33 @@ function syslog_check_upgrade(): void { ); } + if (!syslog_db_table_exists('syslog_status', false)) { + syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_status` ( + `name` varchar(64) NOT NULL default '', + `value` text NOT NULL, + `updated` int(16) NOT NULL default '0', + PRIMARY KEY (`name`)) + ENGINE=InnoDB + ROW_FORMAT=Dynamic"); + } else { + syslog_db_execute("ALTER TABLE `$syslogdb_default`.`syslog_status` MODIFY column `value` TEXT NOT NULL"); + } + + syslog_create_device_rule_table(); + + syslog_create_replication_output_table(); + syslog_create_replication_receipts_table(); + syslog_create_replication_collectors_table(); + syslog_create_replication_recovery_table(); + syslog_ensure_message_text(); + syslog_ensure_replication_history_columns(); +} + + +/** Create dashboard structures and seed the shared starter during install/upgrade. */ +function syslog_dashboard_seed_default(): void { + global $syslogdb_default; + if (!syslog_db_table_exists('syslog_dashboards', false)) { syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_dashboards` ( `id` int(10) NOT NULL auto_increment, @@ -1079,26 +1107,61 @@ function syslog_check_upgrade(): void { ); } - if (!syslog_db_table_exists('syslog_status', false)) { - syslog_db_execute("CREATE TABLE IF NOT EXISTS `$syslogdb_default`.`syslog_status` ( - `name` varchar(64) NOT NULL default '', - `value` text NOT NULL, - `updated` int(16) NOT NULL default '0', - PRIMARY KEY (`name`)) - ENGINE=InnoDB - ROW_FORMAT=Dynamic"); - } else { - syslog_db_execute("ALTER TABLE `$syslogdb_default`.`syslog_status` MODIFY column `value` TEXT NOT NULL"); + $lock = 'syslog_dashboard_' . md5($syslogdb_default); + if (!syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 5)', [$lock])) { + return; } - syslog_create_device_rule_table(); + try { + if (syslog_db_fetch_cell_prepared("SELECT id FROM `$syslogdb_default`.`syslog_dashboards` WHERE hash = ? LIMIT 1", [md5('syslog_default_dashboard')])) { + return; + } - syslog_create_replication_output_table(); - syslog_create_replication_receipts_table(); - syslog_create_replication_collectors_table(); - syslog_create_replication_recovery_table(); - syslog_ensure_message_text(); - syslog_ensure_replication_history_columns(); + if (!syslog_db_execute('START TRANSACTION')) { + return; + } + + $now = time(); + if (!syslog_db_execute_prepared("INSERT INTO `$syslogdb_default`.`syslog_dashboards` + (hash, name, `user`, is_global, `date`, updated) VALUES (?, ?, ?, ?, ?, ?)", [md5('syslog_default_dashboard'), 'default', '', 'on', $now, $now])) { + syslog_db_execute('ROLLBACK'); + return; + } + + $id = (int) syslog_db_fetch_insert_id(); + if ($id <= 0) { + syslog_db_execute('ROLLBACK'); + return; + } + + $panels = [ + [__('Top 10 Hosts', 'syslog'), 'syslog', 'breakdown', 'donut', 'host', 'dashboard'], + [__('Syslog Messages - Past Hour', 'syslog'), 'syslog', 'timeseries', 'area', 'host', '3600'], + [__('Messages by Severity', 'syslog'), 'syslog', 'breakdown', 'donut', 'priority', 'dashboard'], + [__('Top 10 Programs', 'syslog'), 'syslog', 'breakdown', 'donut', 'program', 'dashboard'], + [__('Alert Activity', 'syslog'), 'alerts', 'timeseries', 'bar', 'host', 'dashboard'] + ]; + + foreach ($panels as $position => [$title, $source, $kind, $chart, $field, $timespan]) { + if (!syslog_db_execute_prepared("INSERT INTO `$syslogdb_default`.`syslog_dashboard_panels` + (dashboard_id, title, expression, source, removal, kind, chart, field, + `interval`, timespan, top_n, width, height, position, `date`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [$id, $title, '', $source, 1, $kind, $chart, $field, 'auto', $timespan, 10, 1, 0, $position + 1, $now])) { + syslog_db_execute('ROLLBACK'); + return; + } + } + + if (!syslog_db_execute('COMMIT')) { + syslog_db_execute('ROLLBACK'); + } + } catch (Throwable $error) { + syslog_db_execute('ROLLBACK'); + throw $error; + } finally { + syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', [$lock]); + } } diff --git a/tests/Unit/DashboardDefaultTest.php b/tests/Unit/DashboardDefaultTest.php new file mode 100644 index 00000000..ab189a8a --- /dev/null +++ b/tests/Unit/DashboardDefaultTest.php @@ -0,0 +1,54 @@ + true); + test_override('syslog_db_column_exists', fn () => true); + $GLOBALS['syslogdb_default'] = 'syslog'; + test_override('syslog_dashboard_username', fn () => 'admin'); + test_override('syslog_db_fetch_insert_id', fn () => 42); + $existing = false; + $calls = []; + test_override('syslog_db_fetch_cell_prepared', function ($sql, $params) use (&$existing) { + return str_contains($sql, 'GET_LOCK') || str_contains($sql, 'RELEASE_LOCK') ? 1 : ($existing ? 42 : 0); + }); + test_override('syslog_db_execute', function ($sql) use (&$calls) { + $calls[] = $sql; + return true; + }); + $fail = false; + test_override('syslog_db_execute_prepared', function ($sql, $params) use (&$calls, &$fail) { + $calls[] = [$sql, $params]; + return !$fail || !str_contains($sql, 'syslog_dashboard_panels'); + }); + + syslog_dashboard_seed_default(); + expect($calls[0])->toBe('START TRANSACTION'); + expect($calls[1][1][1])->toBe('default'); + expect($calls[1][1][2])->toBe(''); + expect($calls[1][1][3])->toBe('on'); + expect(end($calls))->toBe('COMMIT'); + $panels = array_slice($calls, 2, 5); + expect($panels)->toHaveCount(5); + foreach ($panels as [$sql, $params]) { + expect(syslog_dashboard_panel_settings([ + 'source' => $params[3], 'removal' => (string) $params[4], + 'kind' => $params[5], 'chart' => $params[6], 'field' => $params[7], + 'interval' => $params[8], 'timespan' => $params[9], 'top_n' => $params[10] + ]))->toBeArray(); + expect($params[2])->toBe(''); + } + expect($panels[1][1][9])->toBe('3600'); + + $calls = []; + $existing = true; + syslog_dashboard_seed_default(); + expect($calls)->toBe([]); + + $existing = false; + $fail = true; + syslog_dashboard_seed_default(); + expect(end($calls))->toBe('ROLLBACK'); + expect($calls)->not->toContain('COMMIT'); +}); From 0d1e761b781d42a368d9ed57574e8f67c83c8386 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Sat, 10 Oct 2026 09:07:19 -0400 Subject: [PATCH 2/3] Add per-panel refresh and remove dashboard arrow controls --- js/dashboard.js | 14 +++++++++----- lib/syslog_dashboard.php | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/js/dashboard.js b/js/dashboard.js index c4c7d0a3..bc178925 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -213,13 +213,14 @@ function syslogDashboardBindResize(handle, card, panel, axis) { } function actionButtons() { - // Shared dashboards are view-only for non-owners. - if (!syslogDashboard.canManage) return []; - var buttons = []; + var actions = [['refresh', 'fa-sync', syslogDashboard.text.refreshPanel]]; + if (syslogDashboard.canManage) { + actions.push(['edit', 'fa-pen', 'Edit'], ['delete', 'fa-trash', 'Delete']); + } // fa-pen: Font Awesome 5 (Cacti 1.2) dropped the FA4 fa-pencil name, // which rendered an empty glyph and made the button look missing. - [['edit', 'fa-pen', 'Edit'], ['up', 'fa-arrow-up', 'Move up'], ['down', 'fa-arrow-down', 'Move down'], ['delete', 'fa-trash', 'Delete']].forEach(function(entry) { + actions.forEach(function(entry) { var button = document.createElement('button'); button.type = 'button'; button.className = 'syslogDashboardCardButton'; @@ -727,7 +728,10 @@ if (window.jQuery) window.jQuery(function() { var panelId = parseInt(this.closest('.syslogDashboardCard').id.replace('syslog_panel_', ''), 10); if (!panelId) return; - if (action === 'edit') { + if (action === 'refresh') { + var panel = syslogDashboardPanelById(panelId); + if (panel) syslogDashboardLoadPanel(panel); + } else if (action === 'edit') { syslogPanelDialogOpen(panelId); } else if (action === 'up' || action === 'down') { syslogDashboardPost({ diff --git a/lib/syslog_dashboard.php b/lib/syslog_dashboard.php index b273c5b9..41d65329 100644 --- a/lib/syslog_dashboard.php +++ b/lib/syslog_dashboard.php @@ -1363,6 +1363,7 @@ function syslog_dashboard(): void { canShare: , shared: , text: { + refreshPanel: , totalMessages: , libraryUnavailable: , editPanel: , From b8aae1b3064a343602b0d239bdf101d267a68b91 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Sat, 10 Oct 2026 09:25:43 -0400 Subject: [PATCH 3/3] Regenerate gettext template for dashboard strings --- locales/po/cacti.pot | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 5cebaa9a..58a080f0 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -6,11 +6,11 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: Cacti 1.3\n" +"Project-Id-Version: Cacti 1.2.32\n" "Report-Msgid-Bugs-To: developers@cacti.net\n" -"POT-Creation-Date: 2026-10-09 15:06-0400\n" +"POT-Creation-Date: 2026-10-10 09:25-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Cacti Developers \n" +"Last-Translator: Cacti Developers >\n" "Language-Team: Cacti Developers \n" "Language: \n" "MIME-Version: 1.0\n" @@ -145,7 +145,7 @@ msgstr "" msgid "Permission denied." msgstr "" -#: includes/functions.php includes/utilities.php +#: includes/functions.php includes/utilities.php syslog.php msgid "Invalid request. Please try again." msgstr "" @@ -1079,6 +1079,14 @@ msgstr "" msgid "A title of up to 128 characters is required." msgstr "" +#: lib/syslog_dashboard.php +msgid "Refresh Panel" +msgstr "" + +#: lib/syslog_dashboard.php +msgid "Total messages" +msgstr "" + #: lib/syslog_dashboard.php msgid "Chart library unavailable on this Cacti installation." msgstr "" @@ -1353,6 +1361,26 @@ msgstr "" msgid "Aria storage cannot be used when any Remote Data Collector option is enabled. Select InnoDB or disable all Remote Data Collector options." msgstr "" +#: setup.php +msgid "Top 10 Hosts" +msgstr "" + +#: setup.php +msgid "Syslog Messages - Past Hour" +msgstr "" + +#: setup.php +msgid "Messages by Severity" +msgstr "" + +#: setup.php +msgid "Top 10 Programs" +msgstr "" + +#: setup.php +msgid "Alert Activity" +msgstr "" + #: syslog.php msgid "Syslog Viewer" msgstr ""