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
38 changes: 20 additions & 18 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,30 @@ When generating code for this repository:

### Key Dependencies
- Cacti core framework (`api_plugin_*`, `db_*`, `read_config_option()`, `get_filter_request_var()`)
- `themes/` CSS overlays, `sounds/` audible alert assets, `js/` client-side dashboard code
- `css/` CSS overlays, `sounds/` audible alert assets, `js/` client-side dashboard code

## Project Structure

```
monitor/ # Repository root (install to plugins/monitor/ in Cacti)
├── js/ # Dashboard client-side logic
├── sounds/ # Alert sound assets
├── themes/ # CSS theme overlays (monitor.css)
├── tests/ # Test suite
├── db_functions.php # SQL filter/join helpers and status/device query utilities
├── monitor.php # Web entrypoint/bootstrap (session/request setup)
├── monitor_controller.php # Action flow, filter handling, page orchestration
├── monitor_render.php # Dashboard/group rendering and view-specific output
├── poller_functions.php # Poller helper logic (uptime checks, notifications, email payloads)
├── poller_monitor.php # Background poller entry point (CLI: --help, --version, --debug)
├── INFO # Plugin metadata (name, version, compat)
monitor/ # Repository root (install to plugins/monitor/ in Cacti)
├── js/ # Dashboard client-side logic
├── sounds/ # Alert sound assets
├── css/ # CSS theme overlays (monitor.css)
├── tests/ # Test suite
├── includes/ # functions.php (DB/query helpers), controller.php (action flow), render.php (rendering), database.php (schema)
├── monitor.php # Web entrypoint/bootstrap (session/request setup)
├── poller_functions.php # Poller helper logic (uptime checks, notifications, email payloads)
├── poller_monitor.php # Background poller entry point (CLI: --help, --version, --debug)
├── INFO # Plugin metadata (name, version, compat)
├── README.md
└── setup.php # Plugin install/uninstall/upgrade hooks, hook registration, config
└── setup.php # Plugin install/uninstall/upgrade hooks, hook registration, config
```

## Naming Conventions

### Function Names
- Procedural functions use **lowerCamelCase**, the `monitor_` prefix, or the required `plugin_monitor_` lifecycle prefix, matching current file conventions — keep plugin hook callback names exactly synchronized between their definitions and their `api_plugin_register_hook()` registration strings.
- Keep top-level entrypoints lightweight; place reusable logic in the appropriate helper file (`db_functions.php`, `poller_functions.php`).
- Keep top-level entrypoints lightweight; place reusable logic in the appropriate helper file (`includes/functions.php`, `poller_functions.php`).

### Database Tables
Plugin tables are prefixed `plugin_monitor_`. Reuse existing table names and avoid introducing parallel schema variants; keep schema evolution inside the existing setup/upgrade lifecycle functions in `setup.php`.
Expand Down Expand Up @@ -100,9 +98,9 @@ Use gettext calls with the `monitor` domain for user-facing strings: `__('Text',
### File Responsibilities
- `setup.php`: plugin lifecycle, hook registration, config arrays/settings, install/upgrade table management.
- `monitor.php`: web entrypoint/bootstrap, includes, session/request setup.
- `monitor_controller.php`: action flow, filter handling, page orchestration.
- `monitor_render.php`: dashboard/group rendering and view-specific output.
- `db_functions.php`: SQL filter/join helpers and status/device query utilities.
- `includes/controller.php`: action flow, filter handling, page orchestration.
- `includes/render.php`: dashboard/group rendering and view-specific output.
- `includes/functions.php`: SQL filter/join helpers and status/device query utilities.
- `poller_monitor.php`: CLI poller entrypoint.
- `poller_functions.php`: poller helper logic (uptime checks, notifications, email payload building).

Expand Down Expand Up @@ -175,3 +173,7 @@ existing code or adding new code, not just in dedicated cleanup passes:
line, `@param` lines, a blank comment line, then `@return`. Infer parameter/return types from
actual usage; don't change the function's real type-hints in the same pass (let static analysis
flag mismatches separately). Skip vendored third-party library files.

## File manifest & upgrade pruning

The plugin ships a root `manifest.json` with three arrays: `tombstones` (files/directories older versions shipped that have since moved or been removed), `expected` (the top-level files and directories that ship today, directories written with a trailing `/`), and `whitelist` (paths holding user data that must never be touched). Keep `expected` current: CI runs `tests/bin/validate-manifest.php`, which fails on any drift between `expected` and the real top-level tree (it ignores `tests/`, `phpunit.xml`, `.git*`, `.md*`, and whitelisted paths). Custom customer CSS/theme files belong in `expected`, and stylesheets live in `css/` (not `themes/`). On upgrade, `monitor_prune_files()` deletes the tombstoned paths, the dev-only `tests/` tree, and the `phpunit.xml` test config, leaves `whitelist`, `.git*`, and `.md*` alone, and logs (without removing) any top-level entry the manifest does not account for. As a safety measure it refuses any tombstone that resolves outside the plugin directory (a tampered manifest.json) and logs a warning for any file or directory it cannot remove. When you move or delete a shipped file, add its old path to `tombstones` and update `expected` in the same change.
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ jobs:
- name: Check PHP version
run: php -v

- name: Validate plugin manifest (expected-file drift)
run: php cacti/plugins/monitor/tests/bin/validate-manifest.php

- name: Run apt-get update
run: sudo apt-get update

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## Changelog

--- 3.0 ---

* feature: Adopt the Cacti plugin table API and relocate schema provisioning into includes/database.php
* feature: Add a fleet-wide manifest.json with upgrade-time file pruning
* feature: Consolidate per-theme override stylesheets from themes/ into css/
* dev: Rename includes/db_functions.php to includes/functions.php and move bundled libraries under includes/
* dev: Set the Cacti compatibility floor to 1.2.29

--- 2.9 ---

* dev: Measure CI coverage with xdebug instead of pcov so the plugin's own sources are instrumented (pcov auto-scopes to the Composer root and skipped cacti/plugins/, leaving the patch-coverage gate with nothing to measure)
Expand Down
4 changes: 2 additions & 2 deletions INFO
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@

[info]
name = monitor
version = 2.9
version = 3.0
longname = Device Monitoring
author = The Cacti Group
email =
homepage = http://www.cacti.net
compat = 1.2.32
compat = 1.2.29
requires = thold:1.2.1
capabilities = online_view:1, online_mgmt:1, offline_view:0, offline_mgmt:0, remote_collect:0
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions monitor_controller.php → includes/controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -1047,8 +1047,8 @@ function monitorGetAjaxStatusLinks(array $host, array $config): string {
}

if (api_plugin_is_enabled('syslog') && api_plugin_user_realm_auth('syslog.php')) {
include($config['base_path'] . '/plugins/syslog/config.php');
include_once($config['base_path'] . '/plugins/syslog/functions.php');
require($config['base_path'] . '/plugins/syslog/config.php');
require_once($config['base_path'] . '/plugins/syslog/functions.php');

$syslog_logs = syslog_db_fetch_cell_prepared(
'SELECT count(*)
Expand Down
204 changes: 204 additions & 0 deletions includes/database.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/

/**
* The plugin_monitor_notify_history table definition (one row per
* notification event). Shared by the create and upgrade paths so both
* stay in sync from a single source.
*
* @return array<string,mixed> Table definition consumed by
* api_plugin_db_table_create()/db_update_table().
*/
function monitor_notify_history_table_data(): array {
$data = [];
$data['columns'][] = ['name' => 'id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'auto_increment' => true];
$data['columns'][] = ['name' => 'host_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => null];
$data['columns'][] = ['name' => 'notify_type', 'type' => 'tinyint(3)', 'unsigned' => true, 'NULL' => true, 'default' => null];
$data['columns'][] = ['name' => 'ping_time', 'type' => 'double', 'NULL' => true, 'default' => null];
$data['columns'][] = ['name' => 'ping_threshold', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => null];
$data['columns'][] = ['name' => 'notification_time', 'type' => 'timestamp', 'NULL' => false, 'default' => '0000-00-00 00:00:00'];
$data['columns'][] = ['name' => 'notes', 'type' => 'varchar(255)', 'NULL' => true, 'default' => null];
$data['primary'] = ['id'];
$data['unique_keys'][] = ['name' => 'unique_key', 'columns' => ['host_id', 'notify_type', 'notification_time']];
$data['type'] = 'InnoDB';
$data['comment'] = 'Stores Notification Event History';

return $data;
}

/**
* The plugin_monitor_reboot_history table definition (one row per detected
* device reboot).
*
* @return array<string,mixed> Table definition.
*/
function monitor_reboot_history_table_data(): array {
$data = [];
$data['columns'][] = ['name' => 'id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'auto_increment' => true];
$data['columns'][] = ['name' => 'host_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => null];
$data['columns'][] = ['name' => 'reboot_time', 'type' => 'timestamp', 'NULL' => false, 'default' => '0000-00-00 00:00:00'];
$data['columns'][] = ['name' => 'log_time', 'type' => 'timestamp', 'NULL' => false, 'default' => 'CURRENT_TIMESTAMP'];
$data['primary'] = ['id'];
$data['keys'][] = ['name' => 'host_id', 'columns' => ['host_id']];
$data['keys'][] = ['name' => 'log_time', 'columns' => ['log_time']];
$data['keys'][] = ['name' => 'reboot_time', 'columns' => ['reboot_time']];
$data['type'] = 'InnoDB';
$data['comment'] = 'Keeps Track of Device Reboot Times';

return $data;
}

/**
* The plugin_monitor_uptime table definition (last known uptime per
* device). Uses the current column layout (uptime NOT NULL).
*
* @return array<string,mixed> Table definition.
*/
function monitor_uptime_table_data(): array {
$data = [];
$data['columns'][] = ['name' => 'host_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'default' => 0];
$data['columns'][] = ['name' => 'uptime', 'type' => 'bigint(20)', 'unsigned' => true, 'NULL' => false, 'default' => 0];
$data['primary'] = ['host_id'];
$data['keys'][] = ['name' => 'uptime', 'columns' => ['uptime']];
$data['type'] = 'InnoDB';
$data['comment'] = 'Keeps Track of the Devices last uptime to track agent restarts and reboots';

return $data;
}

/**
* The plugin_monitor_dashboards table definition (per-user predefined
* dashboards).
*
* @return array<string,mixed> Table definition.
*/
function monitor_dashboards_table_data(): array {
$data = [];
$data['columns'][] = ['name' => 'id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => false, 'auto_increment' => true];
$data['columns'][] = ['name' => 'user_id', 'type' => 'int(10)', 'unsigned' => true, 'NULL' => true, 'default' => 0];
$data['columns'][] = ['name' => 'name', 'type' => 'varchar(128)', 'NULL' => true, 'default' => ''];
$data['columns'][] = ['name' => 'url', 'type' => 'varchar(1024)', 'NULL' => true, 'default' => ''];
$data['primary'] = ['id'];
$data['keys'][] = ['name' => 'user_id', 'columns' => ['user_id']];
$data['type'] = 'InnoDB';
$data['comment'] = 'Stores predefined dashboard information for a user or users';

return $data;
}

/**
* Removes the plugin_db_changes 'create' ownership record for the user-owned
* dashboards table so api_plugin_db_changes_remove() (run by
* api_plugin_uninstall()) does not drop it, preserving user dashboards across
* an uninstall.
*
* @return void
*/
function monitor_disown_dashboards_table() {
db_execute("DELETE FROM plugin_db_changes
WHERE plugin = 'monitor'
AND `table` = 'plugin_monitor_dashboards'
AND method = 'create'");
}

/**
* Creates this plugin's own tables through Cacti's tracked plugin table
* API and ensures the core host table carries the monitoring columns this
* plugin relies on. Called from plugin_monitor_install() and (idempotently)
* from monitor_check_upgrade().
*
* @return void
*/
function monitor_setup_table() {
api_plugin_db_table_create('monitor', 'plugin_monitor_notify_history', monitor_notify_history_table_data());
api_plugin_db_table_create('monitor', 'plugin_monitor_reboot_history', monitor_reboot_history_table_data());
api_plugin_db_table_create('monitor', 'plugin_monitor_uptime', monitor_uptime_table_data());
api_plugin_db_table_create('monitor', 'plugin_monitor_dashboards', monitor_dashboards_table_data());
Comment thread
TheWitness marked this conversation as resolved.

/* dashboards holds user data: create it but do not record plugin ownership, so uninstall preserves it. */
monitor_disown_dashboards_table();

if (db_table_exists('host')) {
$row_format = db_fetch_cell("SELECT ROW_FORMAT
FROM information_schema.tables
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'host'");

if (strtoupper((string) $row_format) !== 'DYNAMIC') {
db_execute('ALTER TABLE host ROW_FORMAT=DYNAMIC');
}
}

db_execute('SET SESSION innodb_strict_mode=0');

api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor', 'type' => 'char(3)', 'NULL' => true, 'default' => 'on']);
api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_text', 'type' => 'text', 'NULL' => false]);
api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_criticality', 'type' => 'tinyint', 'unsigned' => true, 'NULL' => false, 'default' => '0']);
api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_warn', 'type' => 'double', 'NULL' => false, 'default' => '0']);
api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_alert', 'type' => 'double', 'NULL' => false, 'default' => '0']);
api_plugin_db_add_column('monitor', 'host', ['name' => 'monitor_icon', 'type' => 'varchar(30)', 'NULL' => false, 'default' => '']);

db_execute('SET SESSION innodb_strict_mode=1');
}

/**
* Refreshes this plugin's own tables to their current definition on
* upgrade: db_update_table() diffs the live schema against the definition
* and issues the exact ALTER when the table already exists, otherwise the
* table is created outright. Called from monitor_check_upgrade().
*
* @return void
*/
function monitor_upgrade_tables() {
$tables = [
'plugin_monitor_notify_history' => monitor_notify_history_table_data(),
'plugin_monitor_reboot_history' => monitor_reboot_history_table_data(),
'plugin_monitor_uptime' => monitor_uptime_table_data(),
'plugin_monitor_dashboards' => monitor_dashboards_table_data(),
];

foreach ($tables as $table => $table_data) {
if (db_table_exists($table)) {
db_update_table($table, $table_data);
} else {
api_plugin_db_table_create('monitor', $table, $table_data);
}
}

monitor_disown_dashboards_table();
}

/**
* Drops the tables this plugin owns that hold transient monitoring state
* (notification/reboot history and uptime). The user-defined dashboards
* table is intentionally left in place. Called from
* plugin_monitor_uninstall().
*
* @return void
*/
function monitor_drop_tables() {
db_execute('DROP TABLE IF EXISTS plugin_monitor_notify_history');
db_execute('DROP TABLE IF EXISTS plugin_monitor_reboot_history');
db_execute('DROP TABLE IF EXISTS plugin_monitor_uptime');
}
Comment thread
TheWitness marked this conversation as resolved.
File renamed without changes.
File renamed without changes.
Loading
Loading