From 30f266826bc358e456c536a3c8ce2099d82d77ec Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 10:06:02 -0400 Subject: [PATCH 01/14] Initial commit --- .../language/en-GB/mod_healthcheck.ini | 52 ++ .../language/en-GB/mod_healthcheck.sys.ini | 7 + .../mod_healthcheck/mod_healthcheck.xml | 82 +++ .../mod_healthcheck/services/provider.php | 41 ++ .../src/Dispatcher/Dispatcher.php | 57 ++ .../src/Event/HealthChecksEvent.php | 61 ++ .../src/Helper/HealthCheckHelper.php | 519 ++++++++++++++++++ .../modules/mod_healthcheck/tmpl/default.php | 113 ++++ .../css/healthcheck-filter.css | 174 ++++++ .../mod_healthcheck/js/healthcheck-filter.js | 66 +++ layouts/joomla/healthchecks/footer.php | 15 + layouts/joomla/healthchecks/gauge.php | 212 +++++++ layouts/joomla/healthchecks/icon.php | 115 ++++ layouts/joomla/healthchecks/leading.php | 15 + layouts/joomla/healthchecks/list.php | 63 +++ layouts/joomla/healthchecks/table.php | 99 ++++ libraries/src/HTML/Helpers/Healthchecks.php | 373 +++++++++++++ libraries/src/HTML/Registry.php | 1 + 18 files changed, 2065 insertions(+) create mode 100644 administrator/language/en-GB/mod_healthcheck.ini create mode 100644 administrator/language/en-GB/mod_healthcheck.sys.ini create mode 100644 administrator/modules/mod_healthcheck/mod_healthcheck.xml create mode 100644 administrator/modules/mod_healthcheck/services/provider.php create mode 100644 administrator/modules/mod_healthcheck/src/Dispatcher/Dispatcher.php create mode 100644 administrator/modules/mod_healthcheck/src/Event/HealthChecksEvent.php create mode 100644 administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php create mode 100644 administrator/modules/mod_healthcheck/tmpl/default.php create mode 100644 build/media_source/mod_healthcheck/css/healthcheck-filter.css create mode 100644 build/media_source/mod_healthcheck/js/healthcheck-filter.js create mode 100644 layouts/joomla/healthchecks/footer.php create mode 100644 layouts/joomla/healthchecks/gauge.php create mode 100644 layouts/joomla/healthchecks/icon.php create mode 100644 layouts/joomla/healthchecks/leading.php create mode 100644 layouts/joomla/healthchecks/list.php create mode 100644 layouts/joomla/healthchecks/table.php create mode 100644 libraries/src/HTML/Helpers/Healthchecks.php diff --git a/administrator/language/en-GB/mod_healthcheck.ini b/administrator/language/en-GB/mod_healthcheck.ini new file mode 100644 index 00000000000..b30f6abdf43 --- /dev/null +++ b/administrator/language/en-GB/mod_healthcheck.ini @@ -0,0 +1,52 @@ +; Joomla! Project +; (C) 2005 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +MOD_HEALTHCHECK="Health Check" +MOD_HEALTHCHECK_XML_DESCRIPTION="This module shows health check information from installed plugins." + +MOD_HEALTHCHECK_GROUP_FIELD_LABEL="Health Check Group" +MOD_HEALTHCHECK_GROUP_FIELD_DESC="The group (or context) of this module (this value is compared with the group value used in Health Check plugins to inject data)." + +MOD_HEALTHCHECK_HEADER_ICON_FIELD_LABEL="Header Icon" + +; Item types +MOD_HEALTHCHECK_GAUGES="Health Gauges" +MOD_HEALTHCHECK_BUTTONS="Action Buttons" +MOD_HEALTHCHECK_LISTS="Health Lists" +MOD_HEALTHCHECK_TABLES="Data Tables" +MOD_HEALTHCHECK_REPORTS="Health Reports" + +; Status messages +MOD_HEALTHCHECK_STATUS_OK="OK" +MOD_HEALTHCHECK_STATUS_WARNING="Warning" +MOD_HEALTHCHECK_STATUS_ERROR="Error" +MOD_HEALTHCHECK_STATUS_INFO="Information" + +; Default messages +MOD_HEALTHCHECK_LOADING="Loading health check data..." +MOD_HEALTHCHECK_ERROR_LOADING="Error loading health check data" +MOD_HEALTHCHECK_NO_DATA="No health check data available" + +; Accessibility +MOD_HEALTHCHECK_ARIA_GAUGE="Health gauge showing %s" +MOD_HEALTHCHECK_ARIA_BUTTON="Health check action button" +MOD_HEALTHCHECK_ARIA_LIST="Health check list" +MOD_HEALTHCHECK_ARIA_TABLE="Health check data table" +MOD_HEALTHCHECK_ARIA_REPORT="Health check report" + +; Common actions +MOD_HEALTHCHECK_VIEW_DETAILS="View Details" +MOD_HEALTHCHECK_REFRESH="Refresh" +MOD_HEALTHCHECK_EXPORT="Export" +MOD_HEALTHCHECK_CONFIGURE="Configure" + +; Filter buttons +MOD_HEALTHCHECK_FILTER_LABEL="Filter health checks by status" +MOD_HEALTHCHECK_FILTER_ALL="All" +MOD_HEALTHCHECK_FILTER_HEALTHY="Healthy" +MOD_HEALTHCHECK_FILTER_WARNING="Review" +MOD_HEALTHCHECK_FILTER_CRITICAL="Needs Attention" + +MOD_HEALTHCHECK_NO_MATCHING_RESULTS="All good! Nothing to report here." \ No newline at end of file diff --git a/administrator/language/en-GB/mod_healthcheck.sys.ini b/administrator/language/en-GB/mod_healthcheck.sys.ini new file mode 100644 index 00000000000..0f0a719f954 --- /dev/null +++ b/administrator/language/en-GB/mod_healthcheck.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2005 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +MOD_HEALTHCHECK="Health Check" +MOD_HEALTHCHECK_XML_DESCRIPTION="This module shows health check information from installed plugins." \ No newline at end of file diff --git a/administrator/modules/mod_healthcheck/mod_healthcheck.xml b/administrator/modules/mod_healthcheck/mod_healthcheck.xml new file mode 100644 index 00000000000..348f8ab79da --- /dev/null +++ b/administrator/modules/mod_healthcheck/mod_healthcheck.xml @@ -0,0 +1,82 @@ + + + mod_healthcheck + Joomla! Project + 2026-04 + (C) 2026 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 6.2.0 + MOD_HEALTHCHECK_XML_DESCRIPTION + Joomla\Module\Healthcheck + + services + src + tmpl + + + language/en-GB/mod_healthcheck.ini + language/en-GB/mod_healthcheck.sys.ini + + + + +
+ + +
+
+ + + + + + + + + + +
+
+
+
diff --git a/administrator/modules/mod_healthcheck/services/provider.php b/administrator/modules/mod_healthcheck/services/provider.php new file mode 100644 index 00000000000..779ac68ad06 --- /dev/null +++ b/administrator/modules/mod_healthcheck/services/provider.php @@ -0,0 +1,41 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +\defined('_JEXEC') or die; + +use Joomla\CMS\Extension\Service\Provider\HelperFactory; +use Joomla\CMS\Extension\Service\Provider\Module; +use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +/** + * The healthcheck module service provider. + * + * @since __DEPLOY_VERSION__ + */ +return new class () implements ServiceProviderInterface { + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function register(Container $container) + { + $container->registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Healthcheck')); + $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Healthcheck\\Administrator\\Helper')); + + $container->registerServiceProvider(new Module()); + } +}; diff --git a/administrator/modules/mod_healthcheck/src/Dispatcher/Dispatcher.php b/administrator/modules/mod_healthcheck/src/Dispatcher/Dispatcher.php new file mode 100644 index 00000000000..2ebd634bc72 --- /dev/null +++ b/administrator/modules/mod_healthcheck/src/Dispatcher/Dispatcher.php @@ -0,0 +1,57 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Module\Healthcheck\Administrator\Dispatcher; + +use Joomla\CMS\Dispatcher\AbstractModuleDispatcher; +use Joomla\CMS\Helper\HelperFactoryAwareInterface; +use Joomla\CMS\Helper\HelperFactoryAwareTrait; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Dispatcher class for mod_healthcheck + * + * @since __DEPLOY_VERSION__ + */ +class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface +{ + use HelperFactoryAwareTrait; + + /** + * Returns the layout data. + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + protected function getLayoutData() + { + $data = parent::getLayoutData(); + + $helper = $this->getHelperFactory()->getHelper('HealthCheckHelper'); + + $data['gauges'] = $helper->getGauges($data['params'], $this->getApplication()); + $data['buttons'] = $helper->getButtons($data['params'], $this->getApplication()); + $data['lists'] = $helper->getLists($data['params'], $this->getApplication()); + $data['tables'] = $helper->getTables($data['params'], $this->getApplication()); + $data['reports'] = $helper->getReports($data['params'], $this->getApplication()); + + $data['leading'] = $helper->getLeading($data['params'], $this->getApplication()); + $data['footer'] = $helper->getFooter($data['params'], $this->getApplication()); + + // Make helper available to layouts + $data['helper'] = $helper; + + return $data; + } +} diff --git a/administrator/modules/mod_healthcheck/src/Event/HealthChecksEvent.php b/administrator/modules/mod_healthcheck/src/Event/HealthChecksEvent.php new file mode 100644 index 00000000000..441417c9a16 --- /dev/null +++ b/administrator/modules/mod_healthcheck/src/Event/HealthChecksEvent.php @@ -0,0 +1,61 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Module\Healthcheck\Administrator\Event; + +use Joomla\CMS\Event\AbstractEvent; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Event object for retrieving pluggable health checks + * + * @since __DEPLOY_VERSION__ + */ +class HealthChecksEvent extends AbstractEvent +{ + /** + * The event context + * + * @var string + * @since __DEPLOY_VERSION__ + */ + private $context; + + /** + * Get the event context + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getContext() + { + return $this->context; + } + + /** + * Set the event context + * + * @param string $context The event context + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function setContext($context) + { + $this->context = $context; + + return $context; + } +} diff --git a/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php new file mode 100644 index 00000000000..48d7b12035c --- /dev/null +++ b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php @@ -0,0 +1,519 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Module\Healthcheck\Administrator\Helper; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Module\Healthcheck\Administrator\Event\HealthChecksEvent; +use Joomla\Registry\Registry; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Helper for mod_healthcheck + * + * @since __DEPLOY_VERSION__ + */ +class HealthCheckHelper +{ + /** + * Stack to hold gauges + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $gauges = []; + + /** + * Stack to hold buttons + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $buttons = []; + + /** + * Stack to hold lists + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $lists = []; + + /** + * Stack to hold tables + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $tables = []; + + /** + * Stack to hold reports + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $reports = []; + + /** + * Stack to hold leading information + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $leading = []; + + /** + * Stack to hold footer information + * + * @var array[] + * @since __DEPLOY_VERSION__ + */ + protected $footer = []; + + /** + * Generic helper method to get health check data from plugins + * + * @param Registry $params The module parameters + * @param string $type The type of data (gauges, buttons, lists, etc.) + * @param string $eventName The event name to trigger + * @param array $defaults Default values for the data items + * @param array $requiredFields Required fields for validation + * @param CMSApplication|null $application The application + * + * @return array An array of health check data items + * + * @since __DEPLOY_VERSION__ + */ + protected function getHealthCheckData( + Registry $params, + string $type, + string $eventName, + array $defaults, + array $requiredFields, + ?CMSApplication $application = null + ) + { + if ($application == null) { + $application = Factory::getApplication(); + } + + $key = (string) $params; + $context = (string) $params->get('context', 'general'); + $property = $type; // gauges, buttons, lists, etc. + + if (!isset($this->{$property}[$key])) { + // Load mod_healthcheck language file in case this method is called before rendering the module + $application->getLanguage()->load('mod_healthcheck'); + + $this->{$property}[$key] = []; + + PluginHelper::importPlugin('healthcheck'); + + $arrays = (array) $application->triggerEvent( + $eventName, + new HealthChecksEvent($eventName, ['context' => $context]) + ); + + foreach ($arrays as $response) { + if (!\is_array($response)) { + continue; + } + + foreach ($response as $item) { + $item = array_merge($defaults, $item); + + // Validate required fields + $isValid = true; + foreach ($requiredFields as $fieldGroup) { + $hasAnyRequired = false; + foreach ($fieldGroup as $field) { + if (!\is_null($item[$field])) { + $hasAnyRequired = true; + break; + } + } + if (!$hasAnyRequired) { + $isValid = false; + break; + } + } + + if ($isValid) { + $this->{$property}[$key][] = $item; + } + } + } + } + + return $this->{$property}[$key]; + } + + /** + * Helper method to return gauge list. + * + * This method returns the array by reference so it can be + * used to add custom gauges or remove default ones. + * + * @param Registry $params The module parameters + * @param ?CMSApplication $application The application + * + * @return array An array of gauges + * + * @since __DEPLOY_VERSION__ + */ + public function getGauges(Registry $params, ?CMSApplication $application = null) + { + $defaults = [ + 'score' => null, + 'unit' => null, + 'score_min' => null, + 'score_max' => null, + 'score_threshold_warning' => null, + 'score_threshold_success' => null, + 'label' => null, + 'sublabel' => null, + 'note' => null, + 'link' => null, + 'link_title' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', + ]; + + $requiredFields = [ + ['score'], // Must have score + ['unit'] // Must have unit + ]; + + return $this->getHealthCheckData( + $params, + 'gauges', + 'onHealthcheckGetGauges', + $defaults, + $requiredFields, + $application + ); + } + + /** + * Helper method to return button list. + * + * This method returns the array by reference so it can be + * used to add custom buttons or remove default ones. + * + * @param Registry $params The module parameters + * @param ?CMSApplication $application The application + * + * @return array An array of buttons + * + * @since __DEPLOY_VERSION__ + */ + public function getButtons(Registry $params, ?CMSApplication $application = null) + { + $defaults = [ + 'link' => null, + 'image' => null, + 'amount' => null, + 'text' => null, + 'name' => null, + 'linkadd' => null, + 'linkaddicon' => null, + 'access' => true, + 'status' => null, + 'group' => 'general', + ]; + + $requiredFields = [ + ['link'], + ['text', 'name'] // Must have either text or name + ]; + + return $this->getHealthCheckData( + $params, + 'buttons', + 'onHealthcheckGetIcons', + $defaults, + $requiredFields, + $application + ); + } + + /** + * Helper method to return list list. + * + * This method returns the array by reference so it can be + * used to add custom lists or remove default ones. + * + * @param Registry $params The module parameters + * @param ?CMSApplication $application The application + * + * @return array An array of lists + * + * @since __DEPLOY_VERSION__ + */ + public function getLists(Registry $params, ?CMSApplication $application = null) + { + $defaults = [ + 'items' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', + ]; + + $requiredFields = [ + ['items'] + ]; + + return $this->getHealthCheckData( + $params, + 'lists', + 'onHealthcheckGetLists', + $defaults, + $requiredFields, + $application + ); + } + + /** + * Helper method to return table list. + * + * This method returns the array by reference so it can be + * used to add custom tables or remove default ones. + * + * @param Registry $params The module parameters + * @param ?CMSApplication $application The application + * + * @return array An array of tables + * + * @since __DEPLOY_VERSION__ + */ + public function getTables(Registry $params, ?CMSApplication $application = null) + { + $defaults = [ + 'columns' => null, + 'data' => null, + 'caption' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', + 'helper' => $this, + ]; + + $requiredFields = [ + ['columns'], + ['data'] // Must have either text or name + ]; + + return $this->getHealthCheckData( + $params, + 'tables', + 'onHealthcheckGetTables', + $defaults, + $requiredFields, + $application + ); + } + + /** + * Helper method to return report list. + * + * This method returns the array by reference so it can be + * used to add custom reports or remove default ones. + * + * @param Registry $params The module parameters + * @param ?CMSApplication $application The application + * + * @return array An array of reports + * + * @since __DEPLOY_VERSION__ + */ + public function getReports(Registry $params, ?CMSApplication $application = null) + { + $defaults = [ + 'link' => null, + 'image' => null, + 'text' => null, + 'name' => null, + 'linkadd' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', + ]; + + $requiredFields = [ + ['link'], + ['text', 'name'] // Must have either text or name + ]; + + return $this->getHealthCheckData( + $params, + 'reports', + 'onHealthcheckGetReports', + $defaults, + $requiredFields, + $application + ); + } + + /** + * Helper method to return leading information. + * + * @param Registry $params The module parameters + * @param ?CMSApplication $application The application + * + * @return array An array of leading information + * + * @since __DEPLOY_VERSION__ + */ + public function getLeading(Registry $params, ?CMSApplication $application = null) + { + $defaults = [ + 'info' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', + ]; + + $requiredFields = [ + ['info'] // Must have info + ]; + + return $this->getHealthCheckData( + $params, + 'leading', + 'onHealthcheckGetLeading', + $defaults, + $requiredFields, + $application + ); + } + + /** + * Helper method to return footer information. + * + * @param Registry $params The module parameters + * @param ?CMSApplication $application The application + * + * @return array An array of footer information + * + * @since __DEPLOY_VERSION__ + */ + public function getFooter(Registry $params, ?CMSApplication $application = null) + { + $defaults = [ + 'info' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', + ]; + + $requiredFields = [ + ['info'] // Must have info + ]; + + return $this->getHealthCheckData( + $params, + 'footer', + 'onHealthcheckGetFooter', + $defaults, + $requiredFields, + $application + ); + } + + // Function to render cell content based on column type + public function renderTableCellContent($column, $item, $rowIndex) + { + $key = $column['key'] ?? ''; + $type = $column['type'] ?? 'text'; + $value = is_object($item) ? ($item->$key ?? '') : ($item[$key] ?? ''); + + switch ($type) { + case 'badge': + $badgeClass = $column['badgeClass'] ?? 'secondary'; + if (is_callable($column['badgeClass'])) { + $badgeClass = call_user_func($column['badgeClass'], $value, $item, $rowIndex); + } + return '' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . ''; + + case 'link': + $url = $column['url'] ?? ''; + if (is_callable($column['url'])) { + $url = call_user_func($column['url'], $value, $item, $rowIndex); + } + $title = $column['linkTitle'] ?? ''; + if (is_callable($column['linkTitle'])) { + $title = call_user_func($column['linkTitle'], $value, $item, $rowIndex); + } + return '' . + htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . ''; + + case 'date': + $format = $column['dateFormat'] ?? Text::_('DATE_FORMAT_LC4'); + return HTMLHelper::_('date', $value, $format); + + case 'boolean': + $trueText = $column['trueText'] ?? Text::_('JYES'); + $falseText = $column['falseText'] ?? Text::_('JNO'); + $trueClass = $column['trueClass'] ?? 'success'; + $falseClass = $column['falseClass'] ?? 'danger'; + $isTrue = (bool) $value; + return '' . + ($isTrue ? $trueText : $falseText) . ''; + + case 'progress': + $percentage = (float) $value; + $progressClass = $column['progressClass'] ?? 'primary'; + if (is_callable($column['progressClass'])) { + $progressClass = call_user_func($column['progressClass'], $value, $item, $rowIndex); + } + return '
+
' . $percentage . '% +
+
'; + + case 'icon': + $iconClass = $column['iconClass'] ?? 'fas fa-info'; + if (is_callable($column['iconClass'])) { + $iconClass = call_user_func($column['iconClass'], $value, $item, $rowIndex); + } + return ''; + + case 'custom': + if (isset($column['renderer']) && is_callable($column['renderer'])) { + return call_user_func($column['renderer'], $value, $item, $rowIndex, $column); + } + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + + case 'text': + default: + $maxLength = $column['maxLength'] ?? null; + $text = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + if ($maxLength && strlen($text) > $maxLength) { + $text = substr($text, 0, $maxLength) . '...'; + } + return $text; + } + } +} diff --git a/administrator/modules/mod_healthcheck/tmpl/default.php b/administrator/modules/mod_healthcheck/tmpl/default.php new file mode 100644 index 00000000000..3d0f4f8bc0e --- /dev/null +++ b/administrator/modules/mod_healthcheck/tmpl/default.php @@ -0,0 +1,113 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; + +/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ +$wa = $app->getDocument()->getWebAssetManager(); +$wa->useScript('core') + ->useScript('bootstrap.dropdown'); +//$wa->registerAndUseScript('mod_healthcheck', 'mod_healthcheck/healthcheck.min.js', ['relative' => true, 'version' => 'auto'], ['type' => 'module']); +// use mod_quickicon script if buttons are shown + +// Register and use the filter assets +$wa->registerAndUseScript('mod_healthcheck.filter', 'mod_healthcheck/healthcheck-filter.js', [], ['defer' => true], []) + ->registerAndUseStyle('mod_healthcheck.filter', 'mod_healthcheck/healthcheck-filter.css', [], []); + +$gauges_html = HTMLHelper::_('healthchecks.gauges', $gauges); +$buttons_html = HTMLHelper::_('healthchecks.buttons', $buttons); +$lists_html = HTMLHelper::_('healthchecks.lists', $lists); +$tables_html = HTMLHelper::_('healthchecks.tables', $tables); +$reports_html = HTMLHelper::_('healthchecks.reports', $reports); +$leading_html = HTMLHelper::_('healthchecks.leadings', $leading); +$footer_html = HTMLHelper::_('healthchecks.footers', $footer); + +$has_data = !empty($gauges_html) || !empty($buttons_html) || !empty($lists_html) || !empty($tables_html) || !empty($reports_html); +?> + + +
+
+ + + + +
+ +
+ + +
+ +
+ + + +
+ +
+ diff --git a/build/media_source/mod_healthcheck/css/healthcheck-filter.css b/build/media_source/mod_healthcheck/css/healthcheck-filter.css new file mode 100644 index 00000000000..12bece1fa4e --- /dev/null +++ b/build/media_source/mod_healthcheck/css/healthcheck-filter.css @@ -0,0 +1,174 @@ +/** + * @package Joomla.Administrator + * @subpackage mod_healthcheck + * + * @copyright (C) 2026 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/* Filter buttons styling */ + +.healthcheck-filters .btn-group { + display: flex; + justify-content: flex-end; +} + +.healthcheck-filters .btn { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + font-weight: 500; + line-height: 1.5; + border-radius: 0.25rem; + transition: all 0.2s ease-in-out; + flex: 0 0 auto !important; +} + +.healthcheck-filters .btn-group .btn:first-child { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.healthcheck-filters .btn-group .btn:last-child { + border-top-right-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; +} + +.healthcheck-filters .btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.healthcheck-filters .btn.active { + font-weight: 600; +} + +/* Smooth transition for filtered items */ +.quickicon-single, +.quickicon-group { + transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; +} + +.quickicon-single.d-none, +.quickicon-group.d-none { + opacity: 0; + transform: scale(0.95); +} + +/* Responsive adjustments */ +@media (max-width: 576px) { + .healthcheck-filters .btn-group { + flex-direction: column; + width: 100%; + } + + .healthcheck-filters .btn { + border-radius: 0.25rem !important; + margin-bottom: 0.25rem; + } +} + +/* Gauge */ + + .healthcheck-gauge { + margin: 15px; + padding: 15px; + /*border: 1px solid #dee2e6;*/ + border-radius: 8px; + background-color: #fff; + /* Ensure sufficient focus indicator */ + transition: box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out; + /* Make it clear this is focusable */ + cursor: default; + } + +.healthcheck-gauge:focus { + /*outline: none;*/ + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + /*border: 2px solid #0d6efd;*/ +} + +.healthcheck-gauge:focus-within { + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + outline: none; +} + +.gauge-container { + max-width: 200px; + margin: 0 auto; + padding: 10px; +} + +.gauge-svg { + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); +} + +.gauge-label { + font-size: 16px; + font-weight: 600; + color: #495057; +} + +.gauge-sublabel { + font-size: 12px; +} + +.gauge-note { + font-size: 11px; + font-style: italic; + color: #6c757d; +} + +.gauge-debug { + font-family: monospace; + font-size: 10px; + border-top: 1px solid #dee2e6; + padding-top: 5px; + color: #6c757d; +} + +.gauge-link { + position: relative; +} + +.gauge-link::before { + position: absolute; + top:5px; + left:5px; +} + +.gauge-link:hover, +.gauge-link:focus { + color: inherit; + text-decoration: none !important; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + border-radius: 8px; +} + +.gauge-link:focus, +.gauge-link:focus-within { + outline: none; +} + +/* Linked gauges should have pointer cursor */ +.healthcheck-gauge:has(.gauge-link) { + cursor: pointer; +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .healthcheck-gauge { + border: 2px solid; + } + + .gauge-svg circle { + stroke-width: 10; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .gauge-svg circle { + transition: none !important; + } +} diff --git a/build/media_source/mod_healthcheck/js/healthcheck-filter.js b/build/media_source/mod_healthcheck/js/healthcheck-filter.js new file mode 100644 index 00000000000..6dc554fc7ed --- /dev/null +++ b/build/media_source/mod_healthcheck/js/healthcheck-filter.js @@ -0,0 +1,66 @@ +/** + * @package Joomla.Administrator + * @subpackage mod_healthcheck + * + * @copyright (C) 2026 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +document.addEventListener('DOMContentLoaded', function() { + 'use strict'; + + // Get all filter buttons + const filterButtons = document.querySelectorAll('.healthcheck-filters [data-filter]'); + + if (filterButtons.length === 0) { + return; + } + + // Get all health check items that can be filtered + const healthCheckItems = document.querySelectorAll('.quickicon-single[data-filter-status], .quickicon-group[data-filter-status]'); + + // Function to filter items + function filterItems(filterType) { + healthCheckItems.forEach(function(item) { + const itemStatus = item.getAttribute('data-filter-status'); + + if (filterType === 'all') { + // Show all items + item.style.display = ''; + item.classList.remove('d-none'); + } else if (filterType === itemStatus) { + // Show items matching the filter (healthy, warning, or critical) + item.style.display = ''; + item.classList.remove('d-none'); + } else { + // Hide items not matching the filter + item.style.display = 'none'; + item.classList.add('d-none'); + } + }); + } + + // Add click event listeners to filter buttons + filterButtons.forEach(function(button) { + button.addEventListener('click', function(e) { + e.preventDefault(); + + // Remove active class from all buttons + filterButtons.forEach(function(btn) { + btn.classList.remove('active'); + }); + + // Add active class to clicked button + this.classList.add('active'); + + // Get the filter type + const filterType = this.getAttribute('data-filter'); + + // Filter the items + filterItems(filterType); + }); + }); + + // Initialize with 'all' filter + filterItems('all'); +}); diff --git a/layouts/joomla/healthchecks/footer.php b/layouts/joomla/healthchecks/footer.php new file mode 100644 index 00000000000..b9b6bdb5e3d --- /dev/null +++ b/layouts/joomla/healthchecks/footer.php @@ -0,0 +1,15 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; +?> + + + diff --git a/layouts/joomla/healthchecks/gauge.php b/layouts/joomla/healthchecks/gauge.php new file mode 100644 index 00000000000..4b6c6fd0ef2 --- /dev/null +++ b/layouts/joomla/healthchecks/gauge.php @@ -0,0 +1,212 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; + +// Get gauge parameters with defaults +$id = empty($displayData['id']) ? '' : (' id="' . $displayData['id'] . '"'); +$label = $displayData['label'] ?? ''; +$sublabel = $displayData['sublabel'] ?? ''; +$note = $displayData['note'] ?? ''; +$link = $displayData['link'] ?? ''; +$linktitle = $displayData['linktitle'] ?? ''; +$linktarget = ''; + +// Auto-detect external links and set target to _blank +// TODO use Joomla API? +if (!empty($link) && empty($linktarget)) { + // Check if link is external (starts with http/https or contains a different domain) + if (preg_match('/^https?:\/\//', $link)) { + // Extract current domain for comparison + $currentDomain = $_SERVER['HTTP_HOST'] ?? ''; + $linkDomain = parse_url($link, PHP_URL_HOST); + + // If different domain or no current domain info, treat as external + if (empty($currentDomain) || $linkDomain !== $currentDomain) { + $linktarget = '_blank'; + } + } +} +//$rawdata = (float) ($displayData['rawdata'] ?? 0); +$score = (float) ($displayData['score'] ?? 0); +$unit = $displayData['unit'] ?? '%'; +$score_min = (float) ($displayData['score_min'] ?? 0); +$score_max = (float) ($displayData['score_max'] ?? 100); +$score_threshold_error = (float) ($displayData['score_threshold_error'] ?? 0); +$score_threshold_warning = (float) ($displayData['score_threshold_warning'] ?? 50); +$score_threshold_success = (float) ($displayData['score_threshold_success'] ?? 90); + +// Prepare link attributes +$linkAttributes = ''; +$hasLink = !empty($link); +if ($hasLink) { + $linkAttributes = 'href="' . htmlspecialchars($link) . '"'; + if (!empty($linktarget)) { + $linkAttributes .= ' target="' . htmlspecialchars($linktarget) . '"'; + if ($linktarget === '_blank') { + $linkAttributes .= ' rel="noopener noreferrer"'; + } + } + if (!empty($linktitle)) { + $linkAttributes .= ' title="' . htmlspecialchars($linktitle) . '"'; + } else { + $linkAttributes .= ' title="' . htmlspecialchars($label . ' - ' . $score . ' ' . $unit) . '"'; + } +} + +// Calculate percentage for the pie chart +$percentage = ($score_max > $score_min) ? (($score - $score_min) / ($score_max - $score_min)) * 100 : 0; +$percentage = max(0, min(100, $percentage)); // Clamp between 0-100 + +// Determine color based on thresholds +$color = '#dc3545'; // Error (red) +if ($score >= $score_threshold_success) { + $color = '#28a745'; // Success (green) +} elseif ($score >= $score_threshold_warning) { + $color = '#ffc107'; // Warning (yellow) +} + +// Calculate SVG path for pie chart +$radius = 45; +$circumference = 2 * M_PI * $radius; +$strokeDasharray = $circumference; +$strokeDashoffset = $circumference * (1 - $percentage / 100); + +// SVG viewBox and center +$size = 120; +$center = $size / 2; +?> + +
  • + role="img" + tabindex="" + aria-label="" + data-score="" + data-max="" + data-percentage=""> + + + class="gauge-link d-block text-decoration-none" + aria-label=""> + + +
    + +

    + + + +

    + + +
    + + + + +
    + Score: out of . + This represents % of the range from to . + = $score_threshold_success) : ?> + Status: Excellent performance. + = $score_threshold_warning) : ?> + Status: Good performance with room for improvement. + + Status: Performance needs attention. + +
    + + + +
    + + +

    + + + + +
    + Range: - | + Thresholds: // +
    + +
    + + +
    + +
  • diff --git a/layouts/joomla/healthchecks/icon.php b/layouts/joomla/healthchecks/icon.php new file mode 100644 index 00000000000..e27e11606fa --- /dev/null +++ b/layouts/joomla/healthchecks/icon.php @@ -0,0 +1,115 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; + +$id = empty($displayData['id']) ? '' : (' id="' . $displayData['id'] . '"'); +$target = empty($displayData['target']) ? '' : (' target="' . $displayData['target'] . '"'); +$onclick = empty($displayData['onclick']) ? '' : (' onclick="' . $displayData['onclick'] . '"'); + +if (isset($displayData['ajaxurl'])) { + $dataUrl = 'data-url="' . $displayData['ajaxurl'] . '"'; +} else { + $dataUrl = ''; +} + +// The title for the link (a11y) +$title = empty($displayData['title']) ? '' : (' title="' . $this->escape($displayData['title']) . '"'); + +// The information +$text = empty($displayData['text']) ? '' : ('' . $displayData['text'] . ''); + +// Make the class string + +// depending on status, determine additional class +$filterStatus = 'healthy'; // Default to "healthy" +if (isset($displayData['status'])) { + switch ($displayData['status']) { + case 'success': + $class = 'success'; + $filterStatus = 'healthy'; + break; + case 'warning': + $class = 'warning'; + $filterStatus = 'warning'; + break; + case 'error': + $class = 'danger'; + $filterStatus = 'critical'; + break; + default: + $class = 'info'; + $filterStatus = 'healthy'; + } +} +$class .= empty($displayData['class']) ? '' : (' ' . $this->escape($displayData['class'])); +?> + +
  • +
      +
    • + +
    • + + + class="" href=""> +
      +
      + +
      <?php echo empty($displayData['title']) ? '' : $this->escape($displayData['title']); ?>
      + + + +
      + +
      aria-hidden="true"> + +
      +
      + + + +
      +
      +
      + +
      +
      +
      + + +
      + + + + +
      + +
      + + +
    • + + +
    +
  • + diff --git a/layouts/joomla/healthchecks/leading.php b/layouts/joomla/healthchecks/leading.php new file mode 100644 index 00000000000..b9b6bdb5e3d --- /dev/null +++ b/layouts/joomla/healthchecks/leading.php @@ -0,0 +1,15 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; +?> + + + diff --git a/layouts/joomla/healthchecks/list.php b/layouts/joomla/healthchecks/list.php new file mode 100644 index 00000000000..b3cdca65544 --- /dev/null +++ b/layouts/joomla/healthchecks/list.php @@ -0,0 +1,63 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +/** + * Layout variables + * ----------------- + * @var array $items Array of items to display + * @var string $listId Optional list ID + * @var string $listClass Optional additional CSS classes + * @var string $itemClass Optional CSS class for list items + * @var string $type List type: 'ul' (default), 'ol', or 'div' + * @var callable $renderer Optional callback function to render each item + */ + +// Extract layout data +$items = $displayData['items'] ?? []; +$listId = $displayData['id'] ?? ''; +$listClass = $displayData['class'] ?? ''; +$itemClass = $displayData['itemClass'] ?? ''; +$type = $displayData['type'] ?? 'ul'; +$renderer = $displayData['renderer'] ?? null; + +// Build CSS classes +$cssClasses = ['list']; +if (!empty($listClass)) { + $cssClasses[] = $listClass; +} + +// Determine the wrapper tag based on type +$wrapperTag = in_array($type, ['ul', 'ol', 'div']) ? $type : 'ul'; +$itemTag = ($wrapperTag === 'div') ? 'div' : 'li'; +?> +< class=""> + $item) : ?> + <> + title ?? $item->name ?? $item->label ?? 'Item ' . ($index + 1), ENT_QUOTES, 'UTF-8'); + } elseif (is_array($item)) { + echo htmlspecialchars($item['title'] ?? $item['name'] ?? $item['label'] ?? 'Item ' . ($index + 1), ENT_QUOTES, 'UTF-8'); + } else { + echo htmlspecialchars($item, ENT_QUOTES, 'UTF-8'); + } + } + ?> + > + +> diff --git a/layouts/joomla/healthchecks/table.php b/layouts/joomla/healthchecks/table.php new file mode 100644 index 00000000000..1ee2ce0d950 --- /dev/null +++ b/layouts/joomla/healthchecks/table.php @@ -0,0 +1,99 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +/** + * Layout variables + * ----------------- + * @var array $columns Array of column definitions + * @var array $data Array of data rows + * @var string $tableId Optional table ID + * @var string $tableClass Optional additional CSS classes + * @var string $caption Optional table caption for accessibility + * @var bool $striped Whether to add striped rows (default: true) + * @var bool $hover Whether to add hover effect (default: true) + * @var bool $responsive Whether to make table responsive (default: true) + */ + +// Extract layout data +$columns = $displayData['columns'] ?? []; +$data = $displayData['data'] ?? []; +$helper = $displayData['helper'] ?? null; +$tableId = $displayData['id'] ?? ''; +$tableClass = $displayData['class'] ?? ''; +$caption = $displayData['caption'] ?? ''; +$striped = $displayData['striped'] ?? true; +$hover = $displayData['hover'] ?? true; + +// Build table CSS classes +$cssClasses = ['table', 'mt-3']; +if ($striped) { + //$cssClasses[] = 'table-striped'; +} +if ($hover) { + $cssClasses[] = 'table-hover'; +} +if (!empty($tableClass)) { + $cssClasses[] = $tableClass; +} +?> +
    + > + + + + + + + + + + + + + $item) : ?> + + $column) : ?> + + + > + renderTableCellContent($column, $item, $rowIndex); + } else { + // Fallback to basic text rendering + $key = $column['key'] ?? ''; + $value = is_object($item) ? ($item->$key ?? '') : ($item[$key] ?? ''); + echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } + ?> + + + + + +
    + > + +
    +
    diff --git a/libraries/src/HTML/Helpers/Healthchecks.php b/libraries/src/HTML/Helpers/Healthchecks.php new file mode 100644 index 00000000000..12cc285c30e --- /dev/null +++ b/libraries/src/HTML/Helpers/Healthchecks.php @@ -0,0 +1,373 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\HTML\Helpers; + +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Layout\FileLayout; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Utility class for health checks. + * + * @since __DEPLOY_VERSION__ + */ +abstract class Healthchecks +{ + /** + * Method to generate html code for a list of gauges + * + * @param array $gauges Array of gauges + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function gauges($gauges) + { + if (empty($gauges)) { + return ''; + } + + $html = []; + + foreach ($gauges as $gauge) { + $html[] = HTMLHelper::_('healthchecks.gauge', $gauge); + } + + return implode($html); + } + + /** + * Method to generate html code for a gauge + * + * @param array $gauge Gauge properties + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function gauge($gauge) + { + if (!static::canAccess($gauge)) { + return ''; + } + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('joomla.healthchecks.gauge'); + + return $layout->render($gauge); + } + + /** + * Method to generate html code for a list of buttons + * + * @param array $buttons Array of buttons + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function buttons($buttons) + { + if (empty($buttons)) { + return ''; + } + + $html = []; + + foreach ($buttons as $button) { + $html[] = HTMLHelper::_('healthchecks.button', $button); + } + + return implode($html); + } + + /** + * Method to generate html code for a list of buttons + * + * @param array $button Button properties + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function button($button) + { + if (!static::canAccess($button)) { + return ''; + } + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('joomla.healthchecks.icon'); + + return $layout->render($button); + } + + /** + * Method to generate html code for a list of tables + * + * @param array $tables Array of tables + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function tables($tables) + { + if (empty($tables)) { + return ''; + } + + $html = []; + + foreach ($tables as $table) { + $html[] = HTMLHelper::_('healthchecks.table', $table); + } + + return implode($html); + } + + /** + * Method to generate html code for a table + * + * @param array $table Table properties + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function table($table) + { + if (!static::canAccess($table)) { + return ''; + } + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('joomla.healthchecks.table'); + + return $layout->render($table); + } + + /** + * Method to generate html code for a list of lists + * + * @param array $lists Array of lists + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function lists($lists) + { + if (empty($lists)) { + return ''; + } + + $html = []; + + foreach ($lists as $list) { + $html[] = HTMLHelper::_('healthchecks.list', $list); + } + + return implode($html); + } + + /** + * Method to generate html code for a list + * + * @param array $list List properties + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function list($list) + { + if (!static::canAccess($list)) { + return ''; + } + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('joomla.healthchecks.list'); + + return $layout->render($list); + } + + /** + * Method to generate html code for a list of reports + * + * @param array $reports Array of reports + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function reports($reports) + { + if (empty($reports)) { + return ''; + } + + $html = []; + + foreach ($reports as $report) { + $html[] = HTMLHelper::_('healthchecks.report', $report); + } + + return implode($html); + } + + /** + * Method to generate html code for a report + * + * @param array $report Report properties + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function report($report) + { + if (!static::canAccess($report)) { + return ''; + } + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('joomla.healthchecks.report'); + + return $layout->render($report); + } + + /** + * Method to generate html code for a set of leading information + * + * @param array $reports Array of leading information + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function leadings($leadings) + { + if (empty($leadings)) { + return ''; + } + + $html = []; + + foreach ($leadings as $leading) { + $html[] = HTMLHelper::_('healthchecks.leading', $leading); + } + + return implode($html); + } + + /** + * Method to generate html code for leading information + * + * @param array $leading Leading properties + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function leading($leading) + { + if (!static::canAccess($leading)) { + return ''; + } + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('joomla.healthchecks.leading'); + + return $layout->render($leading); + } + + /** + * Method to generate html code for a set of footer information + * + * @param array $reports Array of footer information + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function footers($footers) + { + if (empty($footers)) { + return ''; + } + + $html = []; + + foreach ($footers as $footer) { + $html[] = HTMLHelper::_('healthchecks.footer', $footer); + } + + return implode($html); + } + + /** + * Method to generate html code for footer information + * + * @param array $footer Footer properties + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public static function footer($footer) + { + if (!static::canAccess($footer)) { + return ''; + } + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('joomla.healthchecks.footer'); + + return $layout->render($footer); + } + + /** + * Check if an item can be accessed based on access permissions + * + * @param array $item Item with access properties + * + * @return bool True if access is allowed, false otherwise + * + * @since __DEPLOY_VERSION__ + */ + protected static function canAccess($item) + { + if (!isset($item['access'])) { + return true; + } + + if (\is_bool($item['access'])) { + return $item['access']; + } + + // Get the user object to verify permissions + $user = Factory::getApplication()->getIdentity(); + + // Take each pair of permission, context values. + for ($i = 0, $n = \count($item['access']); $i < $n; $i += 2) { + if (!$user->authorise($item['access'][$i], $item['access'][$i + 1])) { + return false; + } + } + + return true; + } +} diff --git a/libraries/src/HTML/Registry.php b/libraries/src/HTML/Registry.php index 2ca78318c3f..7409372cffb 100644 --- a/libraries/src/HTML/Registry.php +++ b/libraries/src/HTML/Registry.php @@ -43,6 +43,7 @@ final class Registry 'form' => Helpers\Form::class, 'formbehavior' => Helpers\FormBehavior::class, 'grid' => Helpers\Grid::class, + 'healthchecks' => Helpers\HealthChecks::class, 'icons' => Helpers\Icons::class, 'jgrid' => Helpers\JGrid::class, 'jquery' => Helpers\Jquery::class, From 779713f5560791e0be441fd2397ad9644ac974e1 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 10:56:01 -0400 Subject: [PATCH 02/14] Fixed spaces --- .../mod_healthcheck/src/Helper/HealthCheckHelper.php | 3 +-- layouts/joomla/healthchecks/footer.php | 8 ++++---- layouts/joomla/healthchecks/gauge.php | 1 + layouts/joomla/healthchecks/leading.php | 8 ++++---- layouts/joomla/healthchecks/list.php | 7 +++---- layouts/joomla/healthchecks/table.php | 10 +++++----- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php index 48d7b12035c..b15094c44b3 100644 --- a/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php +++ b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php @@ -106,8 +106,7 @@ protected function getHealthCheckData( array $defaults, array $requiredFields, ?CMSApplication $application = null - ) - { + ) { if ($application == null) { $application = Factory::getApplication(); } diff --git a/layouts/joomla/healthchecks/footer.php b/layouts/joomla/healthchecks/footer.php index b9b6bdb5e3d..679b0517944 100644 --- a/layouts/joomla/healthchecks/footer.php +++ b/layouts/joomla/healthchecks/footer.php @@ -9,7 +9,7 @@ */ defined('_JEXEC') or die; -?> - - - + +if (isset($displayData['info'])) { + echo $displayData['info']; +} diff --git a/layouts/joomla/healthchecks/gauge.php b/layouts/joomla/healthchecks/gauge.php index 4b6c6fd0ef2..2963c8c7749 100644 --- a/layouts/joomla/healthchecks/gauge.php +++ b/layouts/joomla/healthchecks/gauge.php @@ -1,4 +1,5 @@ - - - + +if (isset($displayData['info'])) { + echo $displayData['info']; +} diff --git a/layouts/joomla/healthchecks/list.php b/layouts/joomla/healthchecks/list.php index b3cdca65544..747aeead342 100644 --- a/layouts/joomla/healthchecks/list.php +++ b/layouts/joomla/healthchecks/list.php @@ -42,13 +42,12 @@ < class=""> $item) : ?> <> - title ?? $item->name ?? $item->label ?? 'Item ' . ($index + 1), ENT_QUOTES, 'UTF-8'); } elseif (is_array($item)) { diff --git a/layouts/joomla/healthchecks/table.php b/layouts/joomla/healthchecks/table.php index 1ee2ce0d950..d8456764e51 100644 --- a/layouts/joomla/healthchecks/table.php +++ b/layouts/joomla/healthchecks/table.php @@ -45,11 +45,11 @@ $cssClasses[] = $tableClass; } ?> -
    +
    > - + @@ -66,7 +66,7 @@ - + $item) : ?> @@ -80,7 +80,7 @@ ?> > - renderTableCellContent($column, $item, $rowIndex); } else { @@ -94,6 +94,6 @@ - +
    From 82de5c2ded8fe1343d1cd7dd03ec1297e434ad37 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 11:04:32 -0400 Subject: [PATCH 03/14] Fixed spaces --- .../css/healthcheck-filter.css | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/build/media_source/mod_healthcheck/css/healthcheck-filter.css b/build/media_source/mod_healthcheck/css/healthcheck-filter.css index 12bece1fa4e..3f04e75e2b5 100644 --- a/build/media_source/mod_healthcheck/css/healthcheck-filter.css +++ b/build/media_source/mod_healthcheck/css/healthcheck-filter.css @@ -14,28 +14,28 @@ } .healthcheck-filters .btn { - font-size: 0.75rem; - padding: 0.25rem 0.5rem; + padding: .25rem .5rem; + font-size: .75rem; font-weight: 500; line-height: 1.5; - border-radius: 0.25rem; - transition: all 0.2s ease-in-out; + border-radius: .25rem; flex: 0 0 auto !important; + transition: all .2s ease-in-out; } .healthcheck-filters .btn-group .btn:first-child { - border-top-left-radius: 0.375rem; - border-bottom-left-radius: 0.375rem; + border-top-left-radius: .375rem; + border-bottom-left-radius: .375rem; } .healthcheck-filters .btn-group .btn:last-child { - border-top-right-radius: 0.375rem; - border-bottom-right-radius: 0.375rem; + border-top-right-radius: .375rem; + border-bottom-right-radius: .375rem; } .healthcheck-filters .btn:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, .1); transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .healthcheck-filters .btn.active { @@ -45,13 +45,13 @@ /* Smooth transition for filtered items */ .quickicon-single, .quickicon-group { - transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; + transition: opacity .3s ease-in-out, transform .3s ease-in-out; } .quickicon-single.d-none, .quickicon-group.d-none { opacity: 0; - transform: scale(0.95); + transform: scale(.95); } /* Responsive adjustments */ @@ -62,44 +62,43 @@ } .healthcheck-filters .btn { - border-radius: 0.25rem !important; - margin-bottom: 0.25rem; + margin-bottom: .25rem; + border-radius: .25rem !important; } } /* Gauge */ - .healthcheck-gauge { - margin: 15px; - padding: 15px; - /*border: 1px solid #dee2e6;*/ - border-radius: 8px; - background-color: #fff; - /* Ensure sufficient focus indicator */ - transition: box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out; - /* Make it clear this is focusable */ - cursor: default; - } +.healthcheck-gauge { + margin: 15px; + padding: 15px; + background-color: #fff; + border-radius: 8px; + /* Make it clear this is focusable */ + cursor: default; + /* Ensure sufficient focus indicator */ + transition: box-shadow .15s ease-in-out, border-color .15s ease-in-out; +} .healthcheck-gauge:focus { /*outline: none;*/ - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25); /*border: 2px solid #0d6efd;*/ } .healthcheck-gauge:focus-within { - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); outline: none; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25); } .gauge-container { max-width: 200px; - margin: 0 auto; padding: 10px; + margin: 0 auto; } .gauge-svg { - filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .1)); } .gauge-label { @@ -121,8 +120,8 @@ .gauge-debug { font-family: monospace; font-size: 10px; - border-top: 1px solid #dee2e6; padding-top: 5px; + border-top: 1px solid #dee2e6; color: #6c757d; } @@ -140,9 +139,9 @@ .gauge-link:focus { color: inherit; text-decoration: none !important; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0,0,0,0.15); border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, .15); + transform: translateY(-2px); } .gauge-link:focus, From 395faf0a4c2b9a0ad0799d61c4bb699a066bf38e Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 11:12:02 -0400 Subject: [PATCH 04/14] Fixed spaces --- .../mod_healthcheck/css/healthcheck-filter.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build/media_source/mod_healthcheck/css/healthcheck-filter.css b/build/media_source/mod_healthcheck/css/healthcheck-filter.css index 3f04e75e2b5..16f145118c3 100644 --- a/build/media_source/mod_healthcheck/css/healthcheck-filter.css +++ b/build/media_source/mod_healthcheck/css/healthcheck-filter.css @@ -18,8 +18,8 @@ font-size: .75rem; font-weight: 500; line-height: 1.5; - border-radius: .25rem; flex: 0 0 auto !important; + border-radius: .25rem; transition: all .2s ease-in-out; } @@ -70,12 +70,12 @@ /* Gauge */ .healthcheck-gauge { - margin: 15px; padding: 15px; + margin: 15px; background-color: #fff; - border-radius: 8px; /* Make it clear this is focusable */ cursor: default; + border-radius: 8px; /* Ensure sufficient focus indicator */ transition: box-shadow .15s ease-in-out, border-color .15s ease-in-out; } @@ -118,11 +118,11 @@ } .gauge-debug { + padding-top: 5px; font-family: monospace; font-size: 10px; - padding-top: 5px; - border-top: 1px solid #dee2e6; color: #6c757d; + border-top: 1px solid #dee2e6; } .gauge-link { @@ -131,8 +131,8 @@ .gauge-link::before { position: absolute; - top:5px; - left:5px; + top: 5px; + left: 5px; } .gauge-link:hover, From e7680607a497ca659fcbcfee9065799c5d89f818 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 11:14:54 -0400 Subject: [PATCH 05/14] Fixed spaces --- .../src/Helper/HealthCheckHelper.php | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php index b15094c44b3..7525721957b 100644 --- a/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php +++ b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php @@ -111,8 +111,8 @@ protected function getHealthCheckData( $application = Factory::getApplication(); } - $key = (string) $params; - $context = (string) $params->get('context', 'general'); + $key = (string) $params; + $context = (string) $params->get('context', 'general'); $property = $type; // gauges, buttons, lists, etc. if (!isset($this->{$property}[$key])) { @@ -178,20 +178,20 @@ protected function getHealthCheckData( public function getGauges(Registry $params, ?CMSApplication $application = null) { $defaults = [ - 'score' => null, - 'unit' => null, - 'score_min' => null, - 'score_max' => null, - 'score_threshold_warning' => null, - 'score_threshold_success' => null, - 'label' => null, - 'sublabel' => null, - 'note' => null, - 'link' => null, - 'link_title' => null, - 'access' => true, - 'class' => null, - 'group' => 'general', + 'score' => null, + 'unit' => null, + 'score_min' => null, + 'score_max' => null, + 'score_threshold_warning' => null, + 'score_threshold_success' => null, + 'label' => null, + 'sublabel' => null, + 'note' => null, + 'link' => null, + 'link_title' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', ]; $requiredFields = [ @@ -268,10 +268,10 @@ public function getButtons(Registry $params, ?CMSApplication $application = null public function getLists(Registry $params, ?CMSApplication $application = null) { $defaults = [ - 'items' => null, - 'access' => true, - 'class' => null, - 'group' => 'general', + 'items' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', ]; $requiredFields = [ @@ -310,7 +310,7 @@ public function getTables(Registry $params, ?CMSApplication $application = null) 'access' => true, 'class' => null, 'group' => 'general', - 'helper' => $this, + 'helper' => $this, ]; $requiredFields = [ @@ -382,10 +382,10 @@ public function getReports(Registry $params, ?CMSApplication $application = null public function getLeading(Registry $params, ?CMSApplication $application = null) { $defaults = [ - 'info' => null, - 'access' => true, - 'class' => null, - 'group' => 'general', + 'info' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', ]; $requiredFields = [ @@ -415,10 +415,10 @@ public function getLeading(Registry $params, ?CMSApplication $application = null public function getFooter(Registry $params, ?CMSApplication $application = null) { $defaults = [ - 'info' => null, - 'access' => true, - 'class' => null, - 'group' => 'general', + 'info' => null, + 'access' => true, + 'class' => null, + 'group' => 'general', ]; $requiredFields = [ From 91d6fdb7838574893c12084b4572e5451ee17c23 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 11:50:28 -0400 Subject: [PATCH 06/14] Fixed spaces and \ --- .../src/Helper/HealthCheckHelper.php | 60 +++++++++++-------- .../css/healthcheck-filter.css | 5 +- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php index 7525721957b..c98e859e85b 100644 --- a/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php +++ b/administrator/modules/mod_healthcheck/src/Helper/HealthCheckHelper.php @@ -196,7 +196,7 @@ public function getGauges(Registry $params, ?CMSApplication $application = null) $requiredFields = [ ['score'], // Must have score - ['unit'] // Must have unit + ['unit'], // Must have unit ]; return $this->getHealthCheckData( @@ -239,7 +239,7 @@ public function getButtons(Registry $params, ?CMSApplication $application = null $requiredFields = [ ['link'], - ['text', 'name'] // Must have either text or name + ['text', 'name'], // Must have either text or name ]; return $this->getHealthCheckData( @@ -275,7 +275,7 @@ public function getLists(Registry $params, ?CMSApplication $application = null) ]; $requiredFields = [ - ['items'] + ['items'], ]; return $this->getHealthCheckData( @@ -315,7 +315,7 @@ public function getTables(Registry $params, ?CMSApplication $application = null) $requiredFields = [ ['columns'], - ['data'] // Must have either text or name + ['data'], // Must have either text or name ]; return $this->getHealthCheckData( @@ -356,7 +356,7 @@ public function getReports(Registry $params, ?CMSApplication $application = null $requiredFields = [ ['link'], - ['text', 'name'] // Must have either text or name + ['text', 'name'], // Must have either text or name ]; return $this->getHealthCheckData( @@ -389,7 +389,7 @@ public function getLeading(Registry $params, ?CMSApplication $application = null ]; $requiredFields = [ - ['info'] // Must have info + ['info'], // Must have info ]; return $this->getHealthCheckData( @@ -422,7 +422,7 @@ public function getFooter(Registry $params, ?CMSApplication $application = null) ]; $requiredFields = [ - ['info'] // Must have info + ['info'], // Must have info ]; return $this->getHealthCheckData( @@ -438,50 +438,55 @@ public function getFooter(Registry $params, ?CMSApplication $application = null) // Function to render cell content based on column type public function renderTableCellContent($column, $item, $rowIndex) { - $key = $column['key'] ?? ''; - $type = $column['type'] ?? 'text'; + $key = $column['key'] ?? ''; + $type = $column['type'] ?? 'text'; $value = is_object($item) ? ($item->$key ?? '') : ($item[$key] ?? ''); switch ($type) { case 'badge': $badgeClass = $column['badgeClass'] ?? 'secondary'; - if (is_callable($column['badgeClass'])) { - $badgeClass = call_user_func($column['badgeClass'], $value, $item, $rowIndex); + if (\is_callable($column['badgeClass'])) { + $badgeClass = \call_user_func($column['badgeClass'], $value, $item, $rowIndex); } + return '' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . ''; case 'link': $url = $column['url'] ?? ''; - if (is_callable($column['url'])) { - $url = call_user_func($column['url'], $value, $item, $rowIndex); + if (\is_callable($column['url'])) { + $url = \call_user_func($column['url'], $value, $item, $rowIndex); } $title = $column['linkTitle'] ?? ''; - if (is_callable($column['linkTitle'])) { - $title = call_user_func($column['linkTitle'], $value, $item, $rowIndex); + if (\is_callable($column['linkTitle'])) { + $title = \call_user_func($column['linkTitle'], $value, $item, $rowIndex); } + return '' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . ''; case 'date': $format = $column['dateFormat'] ?? Text::_('DATE_FORMAT_LC4'); + return HTMLHelper::_('date', $value, $format); case 'boolean': - $trueText = $column['trueText'] ?? Text::_('JYES'); - $falseText = $column['falseText'] ?? Text::_('JNO'); - $trueClass = $column['trueClass'] ?? 'success'; + $trueText = $column['trueText'] ?? Text::_('JYES'); + $falseText = $column['falseText'] ?? Text::_('JNO'); + $trueClass = $column['trueClass'] ?? 'success'; $falseClass = $column['falseClass'] ?? 'danger'; - $isTrue = (bool) $value; + $isTrue = (bool) $value; + return '' . ($isTrue ? $trueText : $falseText) . ''; case 'progress': $percentage = (float) $value; $progressClass = $column['progressClass'] ?? 'primary'; - if (is_callable($column['progressClass'])) { - $progressClass = call_user_func($column['progressClass'], $value, $item, $rowIndex); + if (\is_callable($column['progressClass'])) { + $progressClass = \call_user_func($column['progressClass'], $value, $item, $rowIndex); } + return '
    From 955ac183b3878ce3e67832ce15429254f2fcdd80 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 13 Apr 2026 15:31:48 -0400 Subject: [PATCH 14/14] Uncommented loading script --- administrator/modules/mod_healthcheck/tmpl/default.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/administrator/modules/mod_healthcheck/tmpl/default.php b/administrator/modules/mod_healthcheck/tmpl/default.php index 3d0f4f8bc0e..f069857b28a 100644 --- a/administrator/modules/mod_healthcheck/tmpl/default.php +++ b/administrator/modules/mod_healthcheck/tmpl/default.php @@ -18,8 +18,7 @@ $wa = $app->getDocument()->getWebAssetManager(); $wa->useScript('core') ->useScript('bootstrap.dropdown'); -//$wa->registerAndUseScript('mod_healthcheck', 'mod_healthcheck/healthcheck.min.js', ['relative' => true, 'version' => 'auto'], ['type' => 'module']); -// use mod_quickicon script if buttons are shown +$wa->registerAndUseScript('mod_quickicon', 'mod_quickicon/quickicon.min.js', ['relative' => true, 'version' => 'auto'], ['type' => 'module']); // Register and use the filter assets $wa->registerAndUseScript('mod_healthcheck.filter', 'mod_healthcheck/healthcheck-filter.js', [], ['defer' => true], [])