From 39fa54e63993cacc6850bd7868682cd0a5a734c6 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 12 Jul 2026 13:42:53 -0700 Subject: [PATCH 1/2] docs: add Monitor architecture decision record --- README.md | 5 ++ docs/architecture/0001-hexagonal-monitor.md | 67 +++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 docs/architecture/0001-hexagonal-monitor.md diff --git a/README.md b/README.md index 0f489c7..97db1cc 100644 --- a/README.md +++ b/README.md @@ -44,5 +44,10 @@ Bug and feature enhancements for the webseer plugin are handled in GitHub. If you find a first search the Cacti forums for a solution before creating an issue in GitHub. +## Architecture + +The proposed architectural direction for Monitor is documented in the +[Architecture Decision Record](docs/architecture/0001-hexagonal-monitor.md). + ----------------------------------------------- Copyright (c) 2004-2026 - The Cacti Group, Inc. diff --git a/docs/architecture/0001-hexagonal-monitor.md b/docs/architecture/0001-hexagonal-monitor.md new file mode 100644 index 0000000..4a6afbb --- /dev/null +++ b/docs/architecture/0001-hexagonal-monitor.md @@ -0,0 +1,67 @@ +# ADR-0001: Migrate Monitor toward a hexagonal architecture + +**Status:** Proposed + +## Context + +Monitor currently combines Cacti hook registration, HTTP request handling, +session state, dashboard rendering, raw SQL construction, poller execution, +notification policy, email delivery, and persistence in a small number of +procedural files. It also stores monitoring configuration in Cacti's shared +`host` table and directly reads optional plugin tables such as Threshold and +Syslog. + +This makes it difficult to test monitoring behavior independently, evolve +state transitions safely, prevent duplicate background work, or maintain a +stable extension boundary while keeping compatibility with Cacti 1.2. + +## Decision + +Monitor will be migrated incrementally to a hexagonal architecture while +preserving the current Cacti 1.2 hooks, UI routes, device fields, and +notification behavior during the compatibility period. + +The target contains the following responsibilities: + +- **Domain:** monitoring profiles, observations, incidents, acknowledgements, + notification attempts, and saved dashboard definitions. +- **Application services:** evaluate a monitoring cycle, reconcile uptime and + reboots, dispatch notifications, query a dashboard, and acknowledge or mute + an incident. +- **Ports:** device-status source, threshold-status source, monitor + repository, dashboard repository, notification gateway, authorization + policy, clock, and worker lease. +- **Adapters:** Cacti host/Threshold/Syslog APIs, MySQL, Cacti mailer, Cacti + poller hook, and the existing Monitor web UI. + +Domain code must not access request variables, sessions, globals, SQL, the +filesystem, or Cacti helper functions. New code must remain PHP 7.4-compatible +until Cacti changes its supported baseline. + +## State and delivery rules + +Availability, latency breach, Threshold trigger, acknowledgement, and user UI +mute state are separate concepts. A monitoring incident has an explicit +lifecycle; a notification attempt and confirmed delivery are distinct records. +Only a lease-owning worker may evaluate a cycle or dispatch its notifications. + +## Migration plan + +1. Add pure value objects and policy functions with characterization tests. +2. Put Cacti host, Threshold, notification, and authorization access behind + adapters used by new application services. +3. Introduce plugin-owned profile, incident, and notification-attempt storage; + mirror existing `host.monitor_*` fields through a compatibility adapter. +4. Move the poller script to a lease-aware evaluation and delivery workflow. +5. Move saved dashboard URL state to a versioned dashboard definition, then + deprecate legacy direct SQL and request/session coupling. + +## Consequences + +- Existing hooks and database fields remain supported during migration rather + than requiring a flag-day rewrite. +- New behavior gains unit, integration, and end-to-end test seams. +- Cacti and optional plugin dependencies become replaceable adapters rather + than hidden global dependencies. +- The migration requires explicit schema/version compatibility and a published + deprecation timeline before legacy paths are removed. From 977ccacdedd0f2f98d71580ebabbf4853da2fc77 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 12 Jul 2026 23:42:33 -0700 Subject: [PATCH 2/2] ci: add code-quality workflow (lint/cs-fixer) Signed-off-by: Thomas Vincent --- .github/workflows/code-quality.yml | 76 +++++++++++ .gitignore | 1 + .php-cs-fixer.php | 195 +++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 .github/workflows/code-quality.yml create mode 100644 .php-cs-fixer.php diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..f9bb742 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,76 @@ +# +-------------------------------------------------------------------------+ +# | Copyright (C) 2004-2025 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/ | +# +-------------------------------------------------------------------------+ + +name: Code Quality + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +permissions: + contents: read + +jobs: + lint: + name: PHP Lint (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Lint PHP sources + run: find . -path ./.git -prune -o -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -r -n1 -P4 php -l + + coding-standards: + name: PHP CS Fixer + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install PHP 8.3 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + tools: php-cs-fixer + + - name: Check coding standards + run: php-cs-fixer fix --dry-run --diff --config=.php-cs-fixer.php diff --git a/.gitignore b/.gitignore index 7a6c551..1238a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ locales/po/*.mo vendor/ .omc/ +.php-cs-fixer.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..4ee4537 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,195 @@ +exclude('vendor') + ->exclude('tests') + ->exclude('locales') + ->exclude('images') + ->exclude('docs') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +$config + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setRiskyAllowed(true) + ->setIndent("\t") + ->setLineEnding("\n") + ->setRules(array( + 'header_comment' => false, + 'comment_to_phpdoc' => true, + 'phpdoc_align' => true, + 'list_syntax' => ['syntax' => 'short'], + 'array_syntax' => ['syntax' => 'short'], + 'trim_array_spaces' => false, + 'no_whitespace_before_comma_in_array' => true, + 'whitespace_after_comma_in_array' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_whitespace_in_blank_line' => true, + 'no_trailing_whitespace' => true, + 'normalize_index_brace' => true, + 'no_mixed_echo_print' => ['use' => 'print'], + 'no_spaces_after_function_name' => true, + 'braces' => [ + 'position_after_functions_and_oop_constructs' => 'same', + 'position_after_control_structures' => 'same', + 'allow_single_line_closure' => true + ], + 'braces_position' => [ + 'anonymous_classes_opening_brace' => 'same_line', + 'anonymous_functions_opening_brace' => 'same_line', + 'classes_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line' + ], + 'single_blank_line_at_eof' => true, + 'method_chaining_indentation' => true, + 'indentation_type' => true, + 'constant_case' => true, + 'lowercase_keywords' => true, + 'line_ending' => true, + 'magic_constant_casing' => true, + 'native_function_casing' => true, + 'elseif' => true, + 'include' => false, + 'no_alternative_syntax' => true, + 'no_superfluous_elseif' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unneeded_braces' => true, + 'no_useless_else' => false, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => null, + 'always_move_variable' => false + ], + 'declare_equal_normalize' => ['space' => 'single'], + 'dir_constant' => true, + 'single_space_around_construct' => [ + 'constructs_followed_by_a_single_space' => [ + 'abstract', + 'as', + 'attribute', + 'break', + 'case', + 'catch', + 'class', + 'clone', + 'const', + 'const_import', + 'continue', + 'do', + 'echo', + 'else', + 'elseif', + 'extends', + 'final', + 'finally', + 'for', + 'foreach', + 'function', + 'function_import', + 'global', + 'goto', + 'if', + 'implements', + 'instanceof', + 'insteadof', + 'interface', + 'match', + 'named_argument', + 'new', + 'open_tag_with_echo', + 'php_open', + 'print', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'throw', + 'trait', + 'try', + 'use', + 'use_lambda', + 'use_trait', + 'var', + 'while', + 'yield', + 'yield_from' + ] + ], + 'concat_space' => ['spacing' => 'one'], + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'switch_continue_to_break' => true, + 'logical_operators' => true, + 'function_declaration' => ['closure_function_spacing' => 'one'], + 'spaces_inside_parentheses' => true, + 'binary_operator_spaces' => [ + 'operators' => [ + '+=' => 'align_single_space', + '===' => 'align_single_space_minimal', + '=' => 'align_single_space', + '|' => 'single_space', + '=>' => 'align', + '!=' => 'align' + ] + ], + 'not_operator_with_space' => false, + 'no_spaces_around_offset' => ['positions' => ['outside', 'inside']], + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'full_opening_tag' => false, + 'linebreak_after_opening_tag' => false, + 'phpdoc_add_missing_param_annotation' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use' + ] + ], + 'no_empty_statement' => true, + 'multiline_whitespace_before_semicolons' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'semicolon_after_instruction' => false, + 'space_after_semicolon' => ['remove_in_empty_for_expressions' => true], + 'blank_line_before_statement' => [ + 'statements' => [ + 'continue', + 'break', + 'declare', + 'do', + 'for', + 'foreach', + 'goto', + 'if', + 'return', + 'switch', + 'throw', + 'try', + 'while', + 'yield', + 'yield_from' + ] + ], + 'explicit_string_variable' => false, + 'single_quote' => true, + 'string_line_ending' => true, + 'strict_param' => true, + 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], + 'single_line_comment_spacing' => true, + 'single_line_comment_style' => true, + 'multiline_comment_opening_closing' => true + )) + ->setFinder($finder); + +return $config;