Skip to content

Initial implementation of a Health Check system and dashboard#47947

Draft
obuisard wants to merge 82 commits into
joomla:6.2-devfrom
obuisard:healthcheck
Draft

Initial implementation of a Health Check system and dashboard#47947
obuisard wants to merge 82 commits into
joomla:6.2-devfrom
obuisard:healthcheck

Conversation

@obuisard

Copy link
Copy Markdown
Contributor

Pull Request resolves # .

  • I read the Generative AI policy and my contribution is either not created with the help of AI or is compatible with the policy and GNU/GPL 2 or later.

Summary of Changes

This PR aims at adding Health Check functionality to Joomla, in a dedicated dashboard.
It lays out the underlying initial implementation and aims at giving instructions to developers to create specific Health Check plugins.

image

Testing Instructions

There is one piece missing, it is the installation of a module healthcheck instance. It is incoming in this draft.
Right now, you need to use the 'discover' functionality to try this PR out.
Thank you for your patience.

Health Check Plugin Developer Guide

This guide explains how to build a healthcheck plugin for Joomla's Health Check module, using the existing usermaintenance plugin as the baseline example.

Relevant core code in this workspace:

  • Module data collection: administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php
  • Module rendering entry point: administrator/modules/mod_healthcheck/tmpl/default.php
  • Layout rendering helper: libraries/src/HTML/Helpers/HealthChecks.php
  • Layout templates:
    • layouts/joomla/healthchecks/icon.php
    • layouts/joomla/healthchecks/gauge.php
    • layouts/joomla/healthchecks/list.php
    • layouts/joomla/healthchecks/table.php
  • Example plugin (icons only): plugins/healthcheck/usermaintenance

1) How the Health Check plugin system works

  1. The module imports the healthcheck plugin group and dispatches events like onHealthcheckGetIcons, onHealthcheckGetGauges, onHealthcheckGetLists, and onHealthcheckGetTables.
  2. Each enabled plugin can subscribe to one or more events and append data into the event result argument.
  3. The module validates required fields and applies defaults (HealthCheckHelper methods getButtons(), getGauges(), getLists(), getTables()).
  4. The HTMLHelper::_('healthchecks.*', ...) helper renders each item through the matching layout file.

The module passes a context value to events. Your plugin should usually ignore events for other contexts.

2) Minimal plugin structure

Use the same structure as plugins/healthcheck/usermaintenance:

plugins/healthcheck/yourplugin/
  yourplugin.xml
  services/provider.php
  src/Extension/YourPlugin.php
  language/en-GB/plg_healthcheck_yourplugin.ini
  language/en-GB/plg_healthcheck_yourplugin.sys.ini
yourplugin.xml essentials
  • group="healthcheck"
  • plugin namespace
  • files and language declarations
  • optional plugin params (including a context text field)

The usermaintenance example defines:

<extension type="plugin" group="healthcheck" method="upgrade">
services/provider.php essentials

Register your plugin as PluginInterface with lazy loading, like plugins/healthcheck/usermaintenance/services/provider.php.

src/Extension/YourPlugin.php essentials
  • extend CMSPlugin
  • implement SubscriberInterface
  • return event subscriptions in getSubscribedEvents()

3) Event subscriptions and payload contracts

Event names
  • Icons: onHealthcheckGetIcons
  • Gauges: onHealthcheckGetGauges
  • Lists: onHealthcheckGetLists
  • Tables: onHealthcheckGetTables
Expected return pattern

Each event method should:

  1. Read current result: $result = $event->getArgument('result', []);
  2. Append one array of items: $result[] = $items;
  3. Write back: $event->setArgument('result', $result);

This matches the pattern in usermaintenance (onHealthcheckGetIcons).

Context guard pattern

Use the same guard style as usermaintenance:

$context = $event->getContext();

if ($context !== $this->params->get('context', 'general')) {
    return;
}

4) Icon items (button layout)

Icons are dispatched through onHealthcheckGetIcons and rendered by layouts/joomla/healthchecks/icon.php.

Required fields

From HealthCheckHelper::getButtons():

  • link (required)
  • one of text or name (required)
Common fields
  • icon (for icon class, used by layout)
  • image (optional image URL)
  • amount (numeric/string badge amount)
  • status (success, warning, error, etc.; affects color/filter)
  • id, class, target, title, onclick
  • group (defaults to general)
  • access (boolean or ACL pair array)
Example (based on usermaintenance)
public function onHealthcheckGetIcons(HealthChecksEvent $event): void
{
    if ($event->getContext() !== $this->params->get('context', 'usermanagement')) {
        return;
    }

    $checks = [];

    $checks[] = [
        'link'   => 'index.php?option=com_users&view=users&filter[state]=1',
        'icon'   => 'fas fa-users-gear',
        'amount' => 12,
        'text'   => 'Inactive users',
        'id'     => 'plg_healthcheck_example_inactive',
        'status' => 'warning',
    ];

    $checks[] = [
        'link'   => 'index.php?option=com_users&view=users&filter[mfa]=0',
        'icon'   => 'fas fa-shield-halved',
        'amount' => 0,
        'text'   => 'Users without MFA',
        'status' => 'success',
    ];

    $result   = $event->getArgument('result', []);
    $result[] = $checks;
    $event->setArgument('result', $result);
}

5) Gauge items

Gauges are dispatched through onHealthcheckGetGauges and rendered by layouts/joomla/healthchecks/gauge.php.

Required fields

From HealthCheckHelper::getGauges():

  • score
  • unit
Useful optional fields
  • label, sublabel, note
  • score_min, score_max
  • score_threshold_warning, score_threshold_success
  • link
  • linktitle (used by layout)
  • group, access, class, id
Example
public function onHealthcheckGetGauges(HealthChecksEvent $event): void
{
    if ($event->getContext() !== $this->params->get('context', 'performance')) {
        return;
    }

    $gauges = [[
        'id'                      => 'plg_healthcheck_example_php_memory',
        'label'                   => 'PHP memory usage',
        'sublabel'                => 'Current process',
        'note'                    => 'Values over 80% should be reviewed.',
        'score'                   => 72,
        'unit'                    => '%',
        'score_min'               => 0,
        'score_max'               => 100,
        'score_threshold_warning' => 70,
        'score_threshold_success' => 90,
        'link'                    => 'index.php?option=com_config',
        'linktitle'               => 'Open Global Configuration',
        'status'                  => 'warning',
    ]];

    $result   = $event->getArgument('result', []);
    $result[] = $gauges;
    $event->setArgument('result', $result);
}

6) List items

Lists are dispatched through onHealthcheckGetLists and rendered by layouts/joomla/healthchecks/list.php.

Required fields

From HealthCheckHelper::getLists():

  • items (array)
Useful optional fields
  • type (ul, ol, or div)
  • class, id, itemClass
  • group, access
Example
public function onHealthcheckGetLists(HealthChecksEvent $event): void
{
    if ($event->getContext() !== $this->params->get('context', 'security')) {
        return;
    }

    $lists = [[
        'id'       => 'plg_healthcheck_example_security_tips',
        'class'    => 'list-group list-group-flush',
        'itemClass'=> 'list-group-item',
        'type'     => 'ul',
        'items'    => [
            'Enable MFA for all administrators',
            'Review inactive accounts monthly',
            'Remove users not assigned to any group',
        ],
    ]];

    $result   = $event->getArgument('result', []);
    $result[] = $lists;
    $event->setArgument('result', $result);
}

7) Table items

Tables are dispatched through onHealthcheckGetTables and rendered by layouts/joomla/healthchecks/table.php.

Required fields

From HealthCheckHelper::getTables():

  • columns (array)
  • data (array)
Column definition basics

Each column typically uses:

  • key (data key)
  • title (header)
  • optional type (text, badge, link, date, boolean, progress, icon, custom)
  • optional display keys (align, width, scope, cellClass, etc.)

type rendering is handled by HealthCheckHelper::renderTableCellContent().

Example
public function onHealthcheckGetTables(HealthChecksEvent $event): void
{
    if ($event->getContext() !== $this->params->get('context', 'usermanagement')) {
        return;
    }

    $tables = [[
        'id'      => 'plg_healthcheck_example_user_risk',
        'caption' => 'Users requiring attention',
        'class'   => 'table-sm',
        'columns' => [
            ['key' => 'name', 'title' => 'User'],
            ['key' => 'lastvisitDate', 'title' => 'Last login', 'type' => 'date'],
            ['key' => 'mfa', 'title' => 'MFA', 'type' => 'boolean'],
            ['key' => 'risk', 'title' => 'Risk', 'type' => 'badge', 'badgeClass' => static function ($value) {
                return $value === 'high' ? 'danger' : ($value === 'medium' ? 'warning' : 'success');
            }],
        ],
        'data' => [
            ['name' => 'Alice Admin', 'lastvisitDate' => '2026-05-14 09:05:00', 'mfa' => 1, 'risk' => 'low'],
            ['name' => 'Bob Manager', 'lastvisitDate' => '2026-01-07 11:21:00', 'mfa' => 0, 'risk' => 'high'],
        ],
    ]];

    $result   = $event->getArgument('result', []);
    $result[] = $tables;
    $event->setArgument('result', $result);
}

8) Full subscriber example

public static function getSubscribedEvents(): array
{
    return [
        'onHealthcheckGetIcons'  => 'onHealthcheckGetIcons',
        'onHealthcheckGetGauges' => 'onHealthcheckGetGauges',
        'onHealthcheckGetLists'  => 'onHealthcheckGetLists',
        'onHealthcheckGetTables' => 'onHealthcheckGetTables',
    ];
}

You can implement only the events you need.

9) Grouping and access control

  • group: defaults to general; use it to classify data by module context strategy.
  • access:
    • true/false for quick allow/deny
    • or ACL checks as pairs, e.g. ['core.manage', 'com_users', 'core.admin', 'com_users']

Access is evaluated in libraries/src/HTML/Helpers/HealthChecks.php (canAccess()).

10) Testing checklist

  1. Install/enable your plugin in group healthcheck.
  2. Ensure the module mod_healthcheck is enabled in Administrator.
  3. Set module context to match your plugin context parameter.
  4. Confirm each subscribed event returns at least one valid item with required fields.
  5. Verify rendering in the module UI:
    • icons appear in the icon section
    • gauges render SVG meters
    • lists render item collections
    • tables render with expected cell types

11) Practical notes from current implementation

  • The usermaintenance plugin currently demonstrates the icon event only.
  • Gauge layout reads linktitle for link title text.
  • The helper default includes link_title for gauges; if you need guaranteed title output in the current layout, set linktitle in your payload.
  • Any missing required field causes that item to be filtered out before rendering.

12) How healthcheck-filters works

The filter bar is part of the module template (administrator/modules/mod_healthcheck/tmpl/default.php), not the plugin itself.

What filters are available

The module renders four filter buttons:

  • all
  • healthy
  • warning
  • critical

Selecting a button shows only matching health-check items in the module.

What plugin authors should set

To make your items filter correctly, provide status on each item payload.

Use these values:

  • success for healthy items
  • warning for warning items
  • error for critical items

If status is omitted, items default to the healthy group.

Where filtering applies

Filtering currently applies to items rendered by these layouts:

  • layouts/joomla/healthchecks/icon.php
  • layouts/joomla/healthchecks/gauge.php
  • layouts/joomla/healthchecks/list.php
  • layouts/joomla/healthchecks/table.php

All filterable items are tagged with data-healthcheck-status, and the module script (media_source/mod_healthcheck/js/healthcheck-filter.js) uses that value to show/hide items.

Status aliases accepted by the module filter

The filter normalization accepts these aliases:

  • success, ok, info -> healthy
  • warning, warn, alert -> warning
  • error, danger -> critical
Quick usage example
$checks[] = [
    'link'   => 'index.php?option=com_users&view=users&filter[mfa]=0',
    'icon'   => 'fas fa-shield-halved',
    'amount' => 3,
    'text'   => 'Users without MFA',
    'status' => 'warning', // appears when the Warning filter is selected
];
Testing filter behavior
  1. Enable your healthcheck plugin and mod_healthcheck.
  2. Return items with a mix of success, warning, and error statuses.
  3. In Administrator, switch between All, Healthy, Warning, and Critical.
  4. Confirm only matching items remain visible for each filter.

Link to documentations

Please select:

  • Documentation link for guide.joomla.org:

  • No documentation changes for guide.joomla.org needed

  • Pull Request link for manual.joomla.org:

  • No documentation changes for manual.joomla.org needed

@joomla-cms-bot joomla-cms-bot added Language Change This is for Translators PR-6.2-dev labels Jun 12, 2026
Comment thread administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php Outdated
Comment thread media_source/mod_healthcheck/css/healthcheck-filter.css Outdated
Comment thread media_source/mod_healthcheck/css/healthcheck-filter.css Outdated
Comment thread plugins/healthcheck/usermaintenance/src/Extension/UserMaintenance.php Outdated
Comment thread layouts/joomla/healthchecks/gauge.php Outdated
Comment thread layouts/joomla/healthchecks/gauge.php Outdated
MOD_HEALTHCHECK_GAUGE_SVG_TITLE="%1$s gauge: %2$s %3$s"
MOD_HEALTHCHECK_GAUGE_SVG_DESC="A circular progress indicator showing %1$s %2$s out of a maximum of %3$s %2$s. This represents %4$s%% of the total range."
MOD_HEALTHCHECK_GAUGE_SR_SCORE="Score: %1$s %2$s out of %3$s %2$s."
MOD_HEALTHCHECK_GAUGE_SR_RANGE="This represents %1$s%% of the range from %2$s to %3$s."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double %% ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed for PHP sprintf escaping so we end with one % and not have PHP trying to parse it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not convinced this is correct but its impossible to test

Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread administrator/language/en-GB/plg_healthcheck_usermaintenance.ini Outdated
Comment thread plugins/healthcheck/usermaintenance/src/Extension/UserMaintenance.php Outdated
Comment thread plugins/healthcheck/usermaintenance/src/Extension/UserMaintenance.php Outdated
obuisard and others added 5 commits June 14, 2026 23:49
…nce.php

Co-authored-by: Christian Heel <66922325+heelc29@users.noreply.github.com>
…nce.php

Co-authored-by: Christian Heel <66922325+heelc29@users.noreply.github.com>
…nce.php

Co-authored-by: Christian Heel <66922325+heelc29@users.noreply.github.com>
…nce.php

Co-authored-by: Christian Heel <66922325+heelc29@users.noreply.github.com>
@brianteeman

Copy link
Copy Markdown
Contributor

Better, I think I will create an additional plugin that highlights all cases, that will not be included in the core.

Please do so that this PR can be really tested - otherwise its not possible for someone to test the entirety of the PR

Comment thread administrator/language/en-GB/mod_healthcheck.sys.ini
@chmst

chmst commented Jun 15, 2026

Copy link
Copy Markdown
Contributor
grafik

The color contrast for review is not sufficient. This color (warning) It is a long existing error in atum, but not visible on other places. (Except during update/migration checks)

@obuisard

Copy link
Copy Markdown
Contributor Author
grafik The color contrast for review is not sufficient. This color (warning) It is a long existing error in atum, but not visible on other places. (Except during update/migration checks)

It looks like we should open a PR for a template fix.

try {
$inactiveTimespan = (int) $this->params->get('inactiveTimespan', 180); // Days since the last login

$item['text'] = Text::_('PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_LISTTEXT');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none of the language strings in this file are present in the pr

@brianteeman

Copy link
Copy Markdown
Contributor

inconsistency in the language strings

Score: 68 % out of 100 %. This represents 68.0% of the range from 0 to 100. Status: Good performance with room for improvement.

  • here we can see that there is a space between the value and the % which should not be there
  • the first instance is to 0 decimal places and the second is to one devimal place
  • as its a % it doesnt even need to say "out of 100%" as it cant be anything else
  • as its a % then the entire "This represents 68.0% of the range from 0 to 100" is redundant

@brianteeman

Copy link
Copy Markdown
Contributor

using link text and title and aria-label is not a good idea. It is generally bad of accessibility espec with screen readers. what are you trying to achieve by doing this

Commit e82837d renamed the per-check *_LISTTEXT / *_LISTNOTE keys to
*_FIELD_LABEL / *_FIELD_DESC in the INI and the XML manifest, but left
UserMaintenance.php still referencing the old *_LISTTEXT / *_LISTNOTE names.
As a result the six user checks rendered raw language keys on the Health
Check dashboard. Point the runtime text/note output at the renamed keys
(no new strings needed; the renamed keys already exist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brianteeman

Copy link
Copy Markdown
Contributor

Thanks for getting the ai to find the issue with the language strings

@brianteeman

Copy link
Copy Markdown
Contributor

This makes extensive use of (often invisible) links with both title and aria-labels to explain the purpose of the link with different levels of information. This can be tricky as screen readers will use one or both which is overkill.

Titles are also invisible to keyboard users and mobile users. On the gauges it's not even obvious that they have a link behind them.

Ideally every link should have anchor text which describe the link and an aria-labels only used where the anchor text is insufficient.

Additional reading https://www.deque.com/blog/text-links-practices-screen-readers/

Comment thread administrator/language/en-GB/mod_healthcheck.ini
PLG_HEALTHCHECK_USERMAINTENANCE_NONMFA_FIELD_LABEL="Active Users Without MFA"
PLG_HEALTHCHECK_USERMAINTENANCE_NEVERLOGGEDIN_FIELD_DESC="Users who created an account, but never actually logged in."
PLG_HEALTHCHECK_USERMAINTENANCE_NEVERLOGGEDIN_FIELD_LABEL="Never Logged In Users"
PLG_HEALTHCHECK_USERMAINTENANCE_ORPHAN_FIELD_DESC="Users who have no user group assigned."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add useless descriptions. This doesn't add anything that is not in the field label

Comment thread administrator/modules/mod_healthcheck/tmpl/default.php Outdated
Comment thread administrator/modules/mod_healthcheck/tmpl/default.php Outdated
?>
<li class="healthcheck-gauge"<?php echo $id; ?>
role="img"
tabindex="<?php echo $hasLink ? '-1' : '0'; ?>"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting tabindex -1 will make it unreachable by a screenreader

Comment thread media_source/mod_healthcheck/css/healthcheck.css Outdated
Comment thread media_source/mod_healthcheck/css/healthcheck.css Outdated
Comment thread plugins/healthcheck/usermaintenance/src/Extension/UserMaintenance.php Outdated
@brianteeman

Copy link
Copy Markdown
Contributor

Please follow the style guide - labels should be capitalised

image

@brianteeman brianteeman mentioned this pull request Jun 19, 2026

@brianteeman brianteeman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the link option=com_users&view=users&filter[state]=1 this should be disabled not inactive users and the description was completely wrong

PLG_HEALTHCHECK_USERMAINTENANCE="HealthCheck - UserMaintenance"
PLG_HEALTHCHECK_USERMAINTENANCE_ACTION_LABEL="Visit the user management"
PLG_HEALTHCHECK_USERMAINTENANCE_CHECKISDEACTIVATED="This check is deactivated in the plugin configuration."
PLG_HEALTHCHECK_USERMAINTENANCE_GETINACTIVE_ERROR="Error while trying to get the number of inactive users."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PLG_HEALTHCHECK_USERMAINTENANCE_GETINACTIVE_ERROR="Error while trying to get the number of inactive users."
PLG_HEALTHCHECK_USERMAINTENANCE_GETINACTIVE_ERROR="Error while trying to get the number of disabled users."

PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVETIMESPAN_VALUE_M6="180"
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVETIMESPAN_VALUE_Y1="360"
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_FIELD_DESC="Users who did not login in the last few months."
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_FIELD_LABEL="Inactive Users"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_FIELD_LABEL="Inactive Users"
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_FIELD_LABEL="Disabled Users"

PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVETIMESPAN_VALUE_M3="90"
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVETIMESPAN_VALUE_M6="180"
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVETIMESPAN_VALUE_Y1="360"
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_FIELD_DESC="Users who did not login in the last few months."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_FIELD_DESC="Users who did not login in the last few months."
PLG_HEALTHCHECK_USERMAINTENANCE_INACTIVE_FIELD_DESC="Users whose accounts are disabled."

}

const amount = parseInt(data.amount, 10) || 0;
amountEl.innerHTML = `<div>${amount}</div>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XSS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Language Change This is for Translators PR-6.2-dev

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants