diff --git a/includes/schema.php b/includes/schema.php index 6025938d..29f15b57 100644 --- a/includes/schema.php +++ b/includes/schema.php @@ -754,6 +754,9 @@ function syslog_setup_table_new(array $options, bool $repair = false): void { syslog_ensure_share_tables(); syslog_ensure_dashboard_tables(); syslog_create_device_rule_table(); + if (!$repair) { + syslog_dashboard_seed_default(); + } if (!$repair) { if (!isset($settings['syslog'])) { diff --git a/js/dashboard.js b/js/dashboard.js index 74517091..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'; @@ -254,7 +255,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; @@ -724,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 bacbf962..41d65329 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,8 @@ function syslog_dashboard(): void { canShare: , shared: , text: { + refreshPanel: , + totalMessages: , libraryUnavailable: , editPanel: , newPanel: , 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 "" diff --git a/setup.php b/setup.php index 57041791..c6c3b456 100644 --- a/setup.php +++ b/setup.php @@ -857,6 +857,7 @@ function syslog_check_upgrade(bool $force = false): bool { if (!syslog_ensure_table_structures()) { return false; } + syslog_dashboard_seed_default(); db_execute_prepared('DELETE FROM plugin_hooks WHERE name = ? AND hook = ?', ['syslog', 'config_insert']); // don't let this script timeout @@ -1008,6 +1009,70 @@ function syslog_check_upgrade(bool $force = false): bool { } +/** Create dashboard structures and seed the shared starter during install/upgrade. */ +function syslog_dashboard_seed_default(): void { + global $syslogdb_default; + + syslog_ensure_dashboard_tables(); + + $lock = 'syslog_dashboard_' . md5($syslogdb_default); + if (!syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 5)', [$lock])) { + return; + } + + try { + if (syslog_db_fetch_cell_prepared("SELECT id FROM `$syslogdb_default`.`syslog_dashboards` WHERE hash = ? LIMIT 1", [md5('syslog_default_dashboard')])) { + return; + } + + 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]); + } +} + + /** * Return the version information for the Syslog plugin. * diff --git a/tests/Unit/DashboardDefaultTest.php b/tests/Unit/DashboardDefaultTest.php new file mode 100644 index 00000000..18e531c7 --- /dev/null +++ b/tests/Unit/DashboardDefaultTest.php @@ -0,0 +1,55 @@ + null); + test_override('syslog_db_table_exists', fn () => 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'); +});