diff --git a/public_html/appmonitor/check-appmonitor-server.php b/public_html/appmonitor/check-appmonitor-server.php
new file mode 100644
index 0000000000000000000000000000000000000000..35bd349c19fbc23d7ad74f95f86ca27976a372a1
--- /dev/null
+++ b/public_html/appmonitor/check-appmonitor-server.php
@@ -0,0 +1,45 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECK
+ * ______________________________________________________________________
+ * 
+ * This is the check file for the appmonitor server installation
+ * Have a look to the docs/client-php.md and index.sample.php
+ * to write your own checks
+ * 
+ * @author: Axel Hahn
+ * ----------------------------------------------------------------------
+ * 2019-04-29  aded check for ssl cert; removed a check
+ * 2019-05-17  aded check http to config- and tmp dir
+ * 2021-11-nn  removed all checks ... created as single files
+ * 2022-03-28  put checks into plugins/apps/
+ */
+
+$sApproot = str_replace('\\', '/', dirname(__DIR__));
+
+require_once($sApproot.'/client/classes/appmonitor-client.class.php');
+
+// require_once('classes/client_all_in_one.php');
+$oMonitor = new appmonitor();
+$oMonitor->setWebsite('Appmonitor server');
+
+// how often the server should ask for updates
+$oMonitor->setTTL(300);
+$oMonitor->addTag('monitoring');
+
+
+// a general include ... the idea is to a file with the same actions on all
+// installations and hosts that can be deployed by a software delivery service 
+// (Puppet, Ansible, ...)
+@include 'general_include.php';
+
+// include default checks for an application
+@require 'plugins/apps/iml-appmonitor-server.php';
+
+// ----------------------------------------------------------------------
+
+$oMonitor->setResult();
+$oMonitor->render();
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/classes/appmonitor-checks.class.php b/public_html/appmonitor/classes/appmonitor-checks.class.php
new file mode 100755
index 0000000000000000000000000000000000000000..865a0e8e041ca1cc0f46d13c2db5aa8d4a8f2d37
--- /dev/null
+++ b/public_html/appmonitor/classes/appmonitor-checks.class.php
@@ -0,0 +1,423 @@
+<?php
+
+if (!defined('RESULT_OK')) {
+    define("RESULT_OK", 0);
+    define("RESULT_UNKNOWN", 1);
+    define("RESULT_WARNING", 2);
+    define("RESULT_ERROR", 3);
+}
+
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * APPMONITOR :: CLASS FOR CLIENT TEST FUNCTIONS<br>
+ * <br>
+ * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br>
+ * LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR <br>
+ * OTHER PARTIES PROVIDE THE PROGRAM ?AS IS? WITHOUT WARRANTY OF ANY KIND, <br>
+ * EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED <br>
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE <br>
+ * ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. <br>
+ * SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY <br>
+ * SERVICING, REPAIR OR CORRECTION.<br>
+ * <br>
+ * --------------------------------------------------------------------------------<br>
+ * <br>
+ * --- HISTORY:<br>
+ * 2014-10-24  0.5   axel.hahn@iml.unibe.ch<br>
+ * 2015-04-08  0.9   axel.hahn@iml.unibe.ch  added sochket test: checkPortTcp<br>
+ * 2018-06-29  0.24  axel.hahn@iml.unibe.ch  add file and directory checks<br>
+ * 2018-07-17  0.42  axel.hahn@iml.unibe.ch  add port on mysqli check<br>
+ * 2018-07-26  0.46  axel.hahn@iml.unibe.ch  fix mysql connection check with empty port param<br>
+ * 2018-08-14  0.47  axel.hahn@iml.unibe.ch  appmonitor client: use timeout of 5 sec for tcp socket connections<br>
+ * 2018-08-15  0.49  axel.hahn@iml.unibe.ch  cert check: added flag to skip verification<br>
+ * 2018-08-23  0.50  axel.hahn@iml.unibe.ch  replace mysqli connect with mysqli real connect (to use a timeout)<br>
+ * 2018-08-27  0.52  axel.hahn@iml.unibe.ch  add pdo connect (starting with mysql)<br>
+ * 2018-11-05  0.58  axel.hahn@iml.unibe.ch  additional flag in http check to show content<br>
+ * 2019-05-31  0.87  axel.hahn@iml.unibe.ch  add timeout as param in connective checks (http, tcp, databases)<br>
+ * 2019-06-05  0.88  axel.hahn@iml.unibe.ch  add plugins<br>
+ * 2021-10-28  0.93  axel.hahn@iml.unibe.ch  add plugins<br>
+ * 2021-12-14  0.93  axel.hahn@iml.unibe.ch  split plugins into single files; added key group in a check<br>
+ * 2023-06-02  0.125 axel.hahn@unibe.ch      replace array_key_exists for better readability
+ * 2024-07-22  0.137 axel.hahn@unibe.ch      php 8 only: use typed variables
+ * --------------------------------------------------------------------------------<br>
+ * @version 0.137
+ * @author Axel Hahn
+ * @link TODO
+ * @license GPL
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL 3.0
+ * @package IML-Appmonitor
+ */
+class appmonitorcheck
+{
+    // ----------------------------------------------------------------------
+    // CONFIG
+    // ----------------------------------------------------------------------
+
+    /**
+     * starting time using microtime
+     * @var float
+     */
+    protected float $_iStart = 0;
+
+    /**
+     * config container
+     * @var array
+     */
+    protected array $_aConfig = [];
+
+    /**
+     * data of all checks
+     * @var array
+     */
+    protected array $_aData = [];
+
+    /**
+     * flat array with units for sizes
+     * @var array
+     */
+    protected array $_units = ['B', 'KB', 'MB', 'GB', 'TB'];
+
+    /**
+     * timeout in sec for tcp socket connections
+     * @var integer
+     */
+    protected int $_iTimeoutTcp = 5;
+
+    /**
+     * point to the plugin directory
+     * @var string
+     */
+    protected string $_sPluginDir = __DIR__ . '/../plugins';
+
+    // ----------------------------------------------------------------------
+    // CONSTRUCTOR
+    // ----------------------------------------------------------------------
+
+    /**
+     * Constructor (nothing here)
+     */
+    public function __construct()
+    {
+
+    }
+
+    // ----------------------------------------------------------------------
+    // PRIVATE FUNCTIONS
+    // ----------------------------------------------------------------------
+
+    /**
+     * Internal: create basic array values for metadata
+     * @return boolean
+     */
+    protected function _createDefaultMetadata(): bool
+    {
+
+        $this->_aData = [
+            "name" => $this->_aConfig["name"],
+            "description" => $this->_aConfig["description"],
+            "group" => isset($this->_aConfig["group"]) ? $this->_aConfig["group"] : false,
+            "parent" => isset($this->_aConfig["parent"]) ? $this->_aConfig["parent"] : false,
+            "result" => RESULT_UNKNOWN,
+            "value" => false,
+            "type" => false,
+            "time" => false,
+        ];
+        return true;
+    }
+
+    /**
+     * Set the result value of a check
+     * @param integer  $iResult  result code; one of RESULT_OK|RESULT_WARNING|RESULT_ERROR|RESULT_UNKNOWN
+     * @return bool
+     */
+    protected function _setResult(int $iResult): bool
+    {
+        $this->_aData["result"] = (int) $iResult;
+        return true;
+    }
+
+    /**
+     * Set a result value of a check
+     * @param string  $s  value; message text for this result
+     * @return bool
+     */
+    protected function _setOutput(string $s): bool
+    {
+        $this->_aData["value"] = $s;
+        return true;
+    }
+
+    /**
+     * Put counter data to result set
+     * @param array  $aParams  array  with possible keys type, count, visual
+     * @return boolean
+     */
+    protected function _setCounter(array $aParams): bool
+    {
+        if (is_array($aParams) && count($aParams)) {
+            foreach (['type', 'count', 'visual'] as $sMyKey) {
+                if (isset($aParams[$sMyKey])) {
+                    $this->_aData[$sMyKey] = $aParams[$sMyKey];
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Set result and output 
+     * @param integer  $iResult   result code; one of RESULT_OK|RESULT_WARNING|RESULT_ERROR|RESULT_UNKNOWN
+     * @param string   $s         message text
+     * @param array    $aCounter  optional: counter with array keys type, count, visual
+     * @return boolean
+     */
+    protected function _setReturn(int $iResult, string $s, array $aCounter = [])
+    {
+        $this->_setResult($iResult);
+        $this->_setOutput($s);
+        $this->_setCounter($aCounter);
+        return true;
+    }
+
+    /**
+     * Check a given array if it contains wanted keys
+     * @param array  $aConfig   array to verify
+     * @param string $sKeyList  key or keys as comma seprated list 
+     * @return boolean
+     */
+    protected function _checkArrayKeys($aConfig, $sKeyList)
+    {
+        foreach (explode(",", $sKeyList) as $sKey) {
+            if (!isset($aConfig[$sKey])) {
+                header('HTTP/1.0 503 Service Unavailable');
+                die('<h1>503 Service Unavailable</h1>'
+                    . '<h2>Details</h2>'
+                    . __METHOD__ . " - array of check parameters requires the keys [$sKeyList] - but key <code>$sKey</code> was not found in config array."
+                    . "<pre>" . print_r($aConfig, true) . '</pre>'
+                );
+            }
+            if (is_null($aConfig[$sKey])) {
+                header('HTTP/1.0 503 Service Unavailable');
+                die('<h1>503 Service Unavailable</h1>'
+                    . '<h2>Details</h2>'
+                    . __METHOD__ . " - key <code>$sKey</code> is empty in config array"
+                    . "<pre>" . print_r($aConfig, true) . '</pre>'
+                );
+            }
+        }
+        return true;
+    }
+
+    // ----------------------------------------------------------------------
+    // PUBLIC FUNCTIONS
+    // ----------------------------------------------------------------------
+
+    /**
+     * Perform a check
+     * @param array $aConfig  configuration array for a check, eg.
+     * <code>
+     * [
+     *     [name] => Dummy
+     *     [description] => Dummy Test
+     *     [check] => [
+     *         [function] => [check function] // i.e. Simple
+     *         [params] => [array]            // optional; arguments for Check function
+     *                                        // its keys depend on the function  
+     *     ]
+     * ]
+     * </code>
+     * @return array
+     */
+    public function makeCheck(array $aConfig): array
+    {
+        $this->_iStart = microtime(true);
+        $this->_checkArrayKeys($aConfig, "name,description,check");
+        $this->_checkArrayKeys($aConfig["check"], "function");
+
+        $this->_aConfig = $aConfig;
+        $this->_createDefaultMetadata();
+
+        $sCheck = preg_replace('/[^a-zA-Z0-9]/', '', $this->_aConfig["check"]["function"]);
+        $aParams = $this->_aConfig["check"]["params"] ?? [];
+
+        // try to load as plugin from a plugin file
+        $sPluginFile = strtolower($this->_sPluginDir . '/checks/' . $sCheck . '.php');
+        // echo "plugin file: $sPluginFile<br>\n";
+        $sCheckClass = 'check' . $sCheck;
+        if (!class_exists($sCheckClass)) {
+            if (file_exists($sPluginFile)) {
+                require_once ($sPluginFile);
+            }
+        }
+
+        if (!class_exists($sCheckClass)) {
+            header('HTTP/1.0 503 Service Unavailable');
+            die('<h1>503 Service Unavailable</h1>'
+                . '<h2>Details</h2>'
+                . __METHOD__ . " - check class not found: <code>$sCheckClass</code>"
+                . "<pre>" . print_r($aConfig, true) . '</pre>'
+                . "<h2>Known checks</h2>\n" . print_r($this->listChecks(), 1)
+            );
+        }
+
+        $oPlugin = new $sCheckClass;
+        $aResponse = $oPlugin->run($aParams);
+        if (!is_array($aResponse)) {
+            header('HTTP/1.0 503 Service Unavailable');
+            die('<h1>503 Service Unavailable</h1>'
+                . '<h2>Details</h2>'
+                . __METHOD__ . " - plugin : $sCheck does not responses an array"
+                . "<pre>INPUT " . print_r($aConfig, true) . '</pre>'
+                . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>'
+            );
+        }
+        if (count($aResponse) < 2) {
+            header('HTTP/1.0 503 Service Unavailable');
+            die('<h1>503 Service Unavailable</h1>'
+                . '<h2>Details</h2>'
+                . __METHOD__ . " - plugin : $sCheck does not responses the minimum of 2 array values"
+                . "<pre>INPUT " . print_r($aConfig, true) . '</pre>'
+                . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>'
+            );
+        }
+        if (!isset($aResponse[2]) || !$aResponse[2]) {
+            $aResponse[2] = [];
+        }
+        $this->_setReturn($aResponse[0], $aResponse[1], $aResponse[2]);
+        if (!$this->_aData['group'] && method_exists($oPlugin, "getGroup")) {
+            $this->_aData['group'] = $oPlugin->getGroup($aParams);
+        }
+
+        $this->_aData['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms';
+        // ... and send response 
+        return $this->respond();
+    }
+
+    /**
+     * List all available checks. This is a helper class you can call
+     * to get an overview over built in functions and plugins. 
+     * You get a flat array with all function names.
+     * @return array
+     */
+    public function listChecks(): array
+    {
+        $aReturn = [];
+        // return internal protected fuctions named "check[whatever]"
+        $class = new ReflectionClass($this);
+        foreach ($class->getMethods(ReflectionMethod::IS_PROTECTED) as $oReflectionMethod) {
+            if (strpos($oReflectionMethod->name, "check") === 0) {
+                $aReturn[(string) $oReflectionMethod->name] = 1;
+            }
+        }
+        // return checks from plugins subdir
+        foreach (glob($this->_sPluginDir . '/checks/*.php') as $sPluginFile) {
+            $aReturn[str_replace('.php', '', basename($sPluginFile))] = 1;
+        }
+        ksort($aReturn);
+        return array_keys($aReturn);
+    }
+
+    /**
+     * Final call of class: send response (data array)
+     * @return array
+     */
+    public function respond()
+    {
+        return $this->_aData;
+    }
+
+    // ----------------------------------------------------------------------
+    // CHECK FUNCTIONS (protected)
+    // ----------------------------------------------------------------------
+
+    /**
+     * Helper function: read certificate data
+     * called in checkCert()
+     * 
+     * @param string  $sUrl         url to connect
+     * @param boolean $bVerifyCert  flag: verify certificate; default: no check
+     * @return array
+     */
+    protected function _certGetInfos(string $sUrl, bool $bVerifyCert): array
+    {
+        $iTimeout = 10;
+        $aUrldata = parse_url($sUrl);
+        $sHost = isset($aUrldata['host']) ? $aUrldata['host'] : false;
+        $iPort = isset($aUrldata['port']) ? $aUrldata['port'] : ((isset($aUrldata['scheme']) && $aUrldata['scheme'] === 'https') ? 443 : false);
+
+        $aSsl = ['capture_peer_cert' => true];
+        if ($bVerifyCert) {
+            $aSsl['verify_peer'] = false;
+            $aSsl['verify_peer_name'] = false;
+        }
+        ;
+        $get = stream_context_create(['ssl' => $aSsl]);
+        if (!$get) {
+            return ['_error' => 'Error: Cannot create stream_context'];
+        }
+        $errno = -1;
+        $errstr = "stream_socket_client failed.";
+        $read = stream_socket_client("ssl://$sHost:$iPort", $errno, $errstr, $iTimeout, STREAM_CLIENT_CONNECT, $get);
+        if (!$read) {
+            return ['_error' => "Error $errno: $errstr; cannot create stream_socket_client with given stream_context to ssl://$sHost:$iPort; you can try to set the flag [verify] to false to check expiration date only."];
+        }
+        $cert = stream_context_get_params($read);
+        if (!$cert) {
+            return ['_error' => "Error: socket was connected to ssl://$sHost:$iPort - but I cannot read certificate infos with stream_context_get_params "];
+        }
+        return openssl_x509_parse($cert['options']['ssl']['peer_certificate']);
+    }
+
+
+    /**
+     * Get human readable space value
+     * @param integer $size
+     * @return string
+     */
+    protected function _getHrSize(int $size): string
+    {
+        $power = $size > 0 ? floor(log($size, 1024)) : 0;
+        return number_format($size / pow(1024, $power), 2, '.', ',') . ' ' . $this->_units[$power];
+    }
+
+    /**
+     * get a space in a real value if an integer has added MB|GB|...
+     * @param string $sValue
+     * @return integer
+     */
+    protected function _getSize(string $sValue): int
+    {
+        if (is_int($sValue)) {
+            return $sValue;
+        }
+        $power = 0;
+        foreach ($this->_units as $sUnit) {
+            if (preg_match('/^[0-9\.\ ]*' . $sUnit . '/', $sValue)) {
+                $i = preg_replace('/([0-9\.]*).*/', '$1', $sValue);
+                $iReal = $i * pow(1024, $power);
+                // die("FOUND: $sValue with unit ${sUnit} - 1024^$power * $i = $iReal");
+                return $iReal;
+            }
+            $power++;
+        }
+        header('HTTP/1.0 503 Service Unavailable');
+        die('<h1>503 Service Unavailable</h1>'
+            . '<h2>Details</h2>'
+            . __METHOD__ . " ERROR in space value parameter - there is no size unit in [$sValue] - allowed size units are " . implode('|', $this->_units)
+        );
+    }
+
+}
diff --git a/public_html/appmonitor/classes/appmonitor-client.class.php b/public_html/appmonitor/classes/appmonitor-client.class.php
new file mode 100755
index 0000000000000000000000000000000000000000..66def5a5d02705512751a93982d325823d7a35f3
--- /dev/null
+++ b/public_html/appmonitor/classes/appmonitor-client.class.php
@@ -0,0 +1,549 @@
+<?php
+if (!class_exists('appmonitorcheck')) {
+    require_once 'appmonitor-checks.class.php';
+}
+
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * APPMONITOR :: CLASS FOR CLIENT CHECKS<br>
+ * <br>
+ * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br>
+ * LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR <br>
+ * OTHER PARTIES PROVIDE THE PROGRAM ?AS IS? WITHOUT WARRANTY OF ANY KIND, <br>
+ * EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED <br>
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE <br>
+ * ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. <br>
+ * SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY <br>
+ * SERVICING, REPAIR OR CORRECTION.<br>
+ * <br>
+ * --------------------------------------------------------------------------------<br>
+ * <br>
+ * --- HISTORY:<br>
+ * 2014-10-24  0.5    axel.hahn@iml.unibe.ch<br>
+ * 2014-11-21  0.6    axel.hahn@iml.unibe.ch  removed meta::ts <br>
+ * 2018-08-23  0.50   axel.hahn@iml.unibe.ch  show version<br>
+ * 2018-08-24  0.51   axel.hahn@iml.unibe.ch  method to show local status page<br>
+ * 2018-08-27  0.52   axel.hahn@iml.unibe.ch  add pdo connect (starting with mysql)<br>
+ * 2018-11-05  0.58   axel.hahn@iml.unibe.ch  additional flag in http check to show content<br>
+ * 2019-05-31  0.87   axel.hahn@iml.unibe.ch  add timeout as param in connective checks (http, tcp, databases)<br>
+ * 2020-05-03  0.110  axel.hahn@iml.unibe.ch  update renderHtmloutput<br>
+ * 2023-07-06  0.128  axel.hahn@unibe.ch      update httpcontent check<br>
+ * 2024-07-19  0.137  axel.hahn@unibe.ch      php 8 only: use typed variables
+ * 2024-11-22  0.141  axel.hahn@unibe.ch      Set client version to server version after updating http, mysqli and app checks
+ * 2025-01-02  0.149  axel.hahn@unibe.ch      add getChecks method
+ * --------------------------------------------------------------------------------<br>
+ * @version 0.149
+ * @author Axel Hahn
+ * @link TODO
+ * @license GPL
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL 3.0
+ * @package IML-Appmonitor
+ */
+class appmonitor
+{
+
+    /**
+     * Name and Version number
+     * @var string
+     */
+    protected string $_sVersion = 'php-client-v0.149';
+
+    /**
+     * config: default ttl for server before requesting the client check again
+     * value is in seconds
+     * @var int
+     */
+    protected int $_iDefaultTtl = 300;
+
+    /**
+     * internal counter: greatest return value of all checks
+     * @var integer 
+     */
+    protected int $_iMaxResult = -1;
+
+    /**
+     * responded metadata of a website
+     * @see _createDefaultMetadata()
+     * @var array
+     */
+    protected array $_aMeta = [];
+
+    /**
+     * Response array of all checks
+     * @see addCheck()
+     * @var array
+     */
+    protected array $_aChecks = [];
+
+    /**
+     * for time measurements: start time
+     * @var float 
+     */
+    protected float $_iStart = 0;
+
+    /**
+     * constructor: init data
+     */
+    public function __construct()
+    {
+        $this->_createDefaultMetadata();
+    }
+
+    // ----------------------------------------------------------------------
+    // protected function
+    // ----------------------------------------------------------------------
+
+    /**
+     * Create basic array values for metadata
+     * @return boolean
+     */
+    protected function _createDefaultMetadata(): bool
+    {
+        $this->_iStart = microtime(true);
+        $this->_aMeta = [
+            "host" => false,
+            "website" => false,
+            "ttl" => false,
+            "result" => false,
+            "time" => false,
+            "version" => $this->_sVersion,
+        ];
+
+        // fill with default values
+        $this->setHost();
+        $this->setWebsite();
+        $this->setTTL();
+        return true;
+    }
+
+    // ----------------------------------------------------------------------
+    // setter
+    // ----------------------------------------------------------------------
+
+    /**
+     * Set the physical hostname for metadata; if no host is given then
+     * the php_uname("n") will be used to set one.
+     * 
+     * @param string $s  hostname
+     * @return bool
+     */
+    public function setHost(string $s = ''): bool
+    {
+        if (!$s) {
+            $s = php_uname("n");
+        }
+        if (!$s) {
+            return false;
+        }
+        $this->_aMeta["host"] = $s;
+        return true;
+    }
+
+    /**
+     * Set a name for this website or application and its environment 
+     * (dev, test, prod); 
+     * 
+     * If you have several application in subdirectories, i.e. /blog,  /shop...
+     * then you should the path or any description to identify them too
+     * 
+     * if no argument is given the name of HTTP_HOST will be used
+     * 
+     * @param string $sWebsite  Name of the website or web application
+     * @return boolean
+     */
+    public function setWebsite($sWebsite = ''): bool
+    {
+        if (!$sWebsite && isset($_SERVER["HTTP_HOST"])) {
+            $sWebsite = $_SERVER["HTTP_HOST"];
+        }
+        if (!$sWebsite) {
+            return false;
+        }
+        $this->_aMeta["website"] = $sWebsite;
+        return true;
+    }
+
+    /**
+     * set a ttl value in seconds to define how long a server should not
+     * ask again for a new status of this instance
+     * 
+     * @param int $iTTl TTL value in sec
+     * @return boolean
+     */
+    public function setTTL($iTTl = 0)
+    {
+        if ($iTTl == 0) {
+            $iTTl = $this->_iDefaultTtl;
+        }
+        return $this->_aMeta["ttl"] = $iTTl;
+    }
+
+    /**
+     * Set final result in meta data; if no value was given then it
+     * sets the biggest value of any check.
+     * 
+     * @param integer  $iResult  set resultcode; one of RESULT_OK|RESULT_WARNING|RESULT_ERROR|RESULT_UNKNOWN
+     * @return boolean
+     */
+    public function setResult(int $iResult = -1): bool
+    {
+        if ($iResult === -1) {
+            $iResult = $this->_iMaxResult; // see addCheck()
+        }
+        $this->_aMeta["result"] = $iResult;
+        return true;
+    }
+
+    /**
+     * Add a check array
+     * @param array  $aJob  array with check data
+     * @return boolean
+     */
+    public function addCheck($aJob = []): bool
+    {
+
+        require_once 'appmonitor-checks.class.php';
+        $oCheck = new appmonitorcheck();
+        $aCheck = $oCheck->makecheck($aJob);
+
+        // limit result code
+        $iMyResult = isset($aJob['worstresult'])
+            ? min($aCheck["result"], $aJob['worstresult'])
+            : $aCheck["result"]
+        ;
+
+        if (!$this->_iMaxResult || $iMyResult > $this->_iMaxResult) {
+            $this->_iMaxResult = $iMyResult;
+        }
+        $this->_aChecks[] = $aCheck;
+        return true;
+    }
+
+    /**
+     * Add an item to notifications meta data
+     * @see addEmail()
+     * @see addSlack()
+     * 
+     * @param string $sType   type ... one of email|slack
+     * @param string $sValue    value
+     * @param string $sKey      optional key (for key->value instead of list of values)
+     * @return boolean
+     */
+    protected function _addNotification(string $sType, string $sValue, string $sKey = ''): bool
+    {
+        $sTypeCleaned = preg_replace('/[^a-z]/', '', strtolower($sType));
+        if (!isset($this->_aMeta['notifications'])) {
+            $this->_aMeta['notifications'] = [];
+        }
+        if (!isset($this->_aMeta['notifications'][$sTypeCleaned])) {
+            $this->_aMeta['notifications'][$sTypeCleaned] = [];
+        }
+        if ($sKey) {
+            $this->_aMeta['notifications'][$sTypeCleaned][$sKey] = $sValue;
+        } else {
+            $this->_aMeta['notifications'][$sTypeCleaned][] = $sValue;
+        }
+        return true;
+    }
+
+    /**
+     * Add an email to notifications list
+     * 
+     * @param string $sEmailAddress  email address to add
+     * @return boolean
+     */
+    public function addEmail(string $sEmailAddress)
+    {
+        return $this->_addNotification('email', $sEmailAddress);
+    }
+
+    /**
+     * Add slack channel for notification
+     * @param string  $sLabel
+     * @param string  $sSlackWebhookUrl
+     * @return boolean
+     */
+    public function addSlackWebhook(string $sLabel, string $sSlackWebhookUrl): bool
+    {
+        return $this->_addNotification('slack', $sSlackWebhookUrl, $sLabel);
+    }
+
+    /**
+     * Add a tag for grouping in the server gui.
+     * Spaces will be replaced with underscore
+     * 
+     * @param string  $sTag  tag to add
+     * @return boolean
+     */
+    public function addTag(string $sTag): bool
+    {
+        if (!isset($this->_aMeta['tags'])) {
+            $this->_aMeta['tags'] = [];
+        }
+        $this->_aMeta['tags'][] = str_replace(' ', '_', $sTag);
+        return true;
+    }
+
+    /**
+     * Check referers IP address if it matches any entry in the list
+     * requires http request; CLI is always allowed
+     * On deny this method exits with 403 response
+     * 
+     * @param array $aAllowedIps  array of allowed ip addresses / ranges
+     *                            the ip must match from the beginning, i.e.
+     *                            "127.0." will allow requests from 127.0.X.Y
+     * @return boolean
+     */
+    public function checkIp(array $aAllowedIps = []): bool
+    {
+        if (!isset($_SERVER['REMOTE_ADDR']) || !count($aAllowedIps)) {
+            return true;
+        }
+        $sIP = $_SERVER['REMOTE_ADDR'];
+        foreach ($aAllowedIps as $sIp2Check) {
+            if (strpos($sIP, $sIp2Check) === 0) {
+                return true;
+            }
+        }
+        header('HTTP/1.0 403 Forbidden');
+        die('ERROR: Your ip address [' . $sIP . '] has no access.');
+    }
+
+    /**
+     * Check a token
+     * requires http request; CLI is always allowed
+     * On deny this method exits with 403 response
+     * 
+     * @param string  $sVarname  name of GET variable
+     * @param string  $sToken    value
+     * @return boolean
+     */
+    public function checkToken(string $sVarname, string $sToken): bool
+    {
+        if (!isset($_GET)) {
+            return true;
+        }
+        if (isset($_GET[$sVarname]) && $_GET[$sVarname] === $sToken) {
+            return true;
+        }
+        header('HTTP/1.0 403 Forbidden');
+        die('ERROR: A token is required.');
+    }
+
+    // ----------------------------------------------------------------------
+    // getter
+    // ----------------------------------------------------------------------
+
+    /**
+     * list all available check functions. This is a helper class you cann call
+     * to get an overview over built in functions. You get a flat array with
+     * all function names.
+     * @return array
+     */
+    public function listChecks(): array
+    {
+        require_once 'appmonitor-checks.class.php';
+        $oCheck = new appmonitorcheck();
+        return $oCheck->listChecks();
+    }
+
+    // ----------------------------------------------------------------------
+    // checks
+    // ----------------------------------------------------------------------
+
+    /**
+     * verify array values and in case of an error abort and show all found errors
+     * @return boolean
+     */
+    protected function _checkData(): bool
+    {
+        $aErrors = [];
+
+        if (!count($this->_aChecks)) {
+            $aErrors[] = "No checks have been defined.";
+        }
+
+        if ($this->_aMeta["result"] === false) {
+            $aErrors[] = "method setResult was not used to set a final result for all checks.";
+        }
+
+        if (count($aErrors)) {
+            $this->abort(
+                '<h2>Error: client check is not complete</h2><p>Found errors:</p><ol><li>' . implode('<li>', $aErrors) . '</ol><br><br>'
+                // .'Dump of your data so far:<pre>' . json_encode($this->getResults(), JSON_PRETTY_PRINT) . '</pre><hr>'
+            );
+        }
+        return true;
+    }
+
+    // ----------------------------------------------------------------------
+    // output
+    // ----------------------------------------------------------------------
+
+    /**
+     * Stop processing the client checks and abort with an error
+     * @param string $sMessage  text to show after a 503 headline
+     * @return void
+     */
+    public function abort(string $sMessage): void
+    {
+        header('HTTP/1.0 503 Service Unavailable');
+        die('<h1>503 Service Unavailable</h1>' . $sMessage);
+    }
+
+    /**
+     * Get array with executed checks
+     * @return array
+     */
+    public function getChecks(): array
+    {
+        return $this->_aChecks;
+    }
+
+    /**
+     * Get full array for response with metadata and checks
+     * @return array
+     */
+    public function getResults(): array
+    {
+        return [
+            "meta" => $this->_aMeta,
+            "checks" => $this->_aChecks,
+        ];
+    }
+
+    /**
+     * Send http response with header and appmonitor JSON data
+     * @return string
+     */
+    public function render(): string
+    {
+        $this->_checkData();
+        $this->_aMeta['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms';
+        $sOut=json_encode($this->getResults());
+
+        header('Content-type: application/json');
+        header('Cache-Control: cache');
+        header('max-age: ' . $this->_aMeta["ttl"]);
+        echo $sOut;
+        return $sOut;
+    }
+
+    /**
+     * Output appmonitor client status as single html page
+     * 
+     * @example <code>
+     * ob_start();<br>
+     * require __DIR__ . '/appmonitor/client/index.php';
+     * $sJson=ob_get_contents();
+     * ob_end_clean();
+     * $oMonitor->renderHtmloutput($sJson);
+     * </code>
+     * 
+     * @param string  $sJson  JSON of client output
+     * @return string
+     */
+    public function renderHtmloutput(string $sJson): string
+    {
+
+        header('Content-type: text/html');
+        header('Cache-Control: cache');
+        header('max-age: ' . $this->_aMeta["ttl"]);
+        $aMsg = [
+            0 => "OK",
+            1 => "UNKNOWN",
+            2 => "WARNING",
+            3 => "ERROR"
+        ];
+
+        // $sOut = print_r($sJson, 1);
+        $aData = json_decode($sJson, 1);
+
+        // ----- Ausgabe human readable
+        $sOut = '';
+        $sOut .= ''
+            . '<h2>Metadata</h2>'
+            . '<div class="meta' . (isset($aData['meta']['result']) ? ' result' . $aData['meta']['result'] : '') . '">'
+            . 'Status: ' . (isset($aData['meta']['result']) ? $aMsg[$aData['meta']['result']] : '?') . '<br>'
+            . '</div>'
+            . 'Host: ' . (isset($aData['meta']['host']) ? '<span class="string">' . $aData['meta']['host'] . '</span>' : '?') . '<br>'
+            . 'Website: ' . (isset($aData['meta']['website']) ? '<span class="string">' . $aData['meta']['website'] . '</span>' : '?') . '<br>'
+            . 'Execution time: ' . (isset($aData['meta']['time']) ? '<span class="float">' . $aData['meta']['time'] . '</span>' : '?') . '<br>'
+            . 'Client: ' . (isset($aData['meta']['version']) ? '<span class="string">' . $aData['meta']['version'] . '</span>' : '?') . '<br>'
+
+            . '<h2>Checks</h2>'
+        ;
+        if (isset($aData['checks'][0]) && count($aData['checks'])) {
+            foreach ($aData['checks'] as $aCheck) {
+                $sOut .= ''
+                    . '<span class="result' . $aCheck['result'] . '"> <strong>' . $aCheck['name'] . '</strong></span> <br>'
+                    . '<div class="check">'
+                    . '<div class="description">'
+                    . $aCheck['description'] . '<br>'
+                    . $aCheck['value'] . '<br>'
+                    . '</div>'
+                    . 'Execution time: <span class="float">' . (isset($aCheck['time']) ? $aCheck['time'] : ' - ') . '</span><br>'
+                    . 'Group: <span class="string">' . (isset($aCheck['group']) ? $aCheck['group'] : '-') . '</span><br>'
+                    . 'parent: <span class="string">' . (isset($aCheck['parent']) ? $aCheck['parent'] : '-') . '</span><br>'
+                    . 'Status: ' . $aMsg[$aCheck['result']] . '<br>'
+                    . '</div>'
+                ;
+            }
+        }
+        $sOut .= '<h2>List of farbcodes</h2>';
+        foreach ($aMsg as $i => $sText) {
+            $sOut .= '<span class="result' . $i . '">' . $sText . '</span> ';
+        }
+
+        $sRaw=json_encode($aData, JSON_PRETTY_PRINT);
+        $sRaw = preg_replace('/:\ \"(.*)\"/U', ': "<span class="string">$1</span>"', $sRaw);
+        $sRaw = preg_replace('/:\ ([0-9]*)/', ': <span class="int">$1</span>', $sRaw);
+        $sRaw = preg_replace('/\"(.*)\":/U', '"<span class="key">$1</span>":', $sRaw);
+
+        $sOut .= '<h2>Raw result data</h2><pre id="raw">' . $sRaw . '</pre>';
+
+
+        $sOut = '<!DOCTYPE html><html><head>'
+            . '<style>'
+            . 'body{background:#eee; color:#444; font-family: verdana,arial; margin: 0; }'
+            . 'body>div#content{background: #fff; border-radius: 2em; border: 4px solid #abc; box-shadow: 0.5em 0.5em 2em #aaa; margin: 2em 10%; padding: 2em;}'
+            . 'h1{color:#346; margin: 0;}'
+            . 'h2{color:#569; margin-top: 2em;}'
+            . 'pre{background:#f4f4f8; padding: 1em; overflow-x:auto; }'
+            . '#raw .key{color:#808;}'
+            . '#raw .int{color:#3a3; font-weight: bold;}'
+            . '#raw .string{color:#66e;}'
+            . '.check{border: 1px solid #ccc; padding: 0.4em; margin-bottom: 2em;}'
+            . '.description{font-style: italic; padding: 0.4em 1em;}'
+            . '.float{color:#080;}'
+            . '.meta{margin-bottom: 1em;}'
+            . '.result0{background:#aca; border-left: 1em solid #080; padding: 0.5em; }'
+            . '.result1{background:#ccc; border-left: 1em solid #aaa; padding: 0.5em; }'
+            . '.result2{background:#fc9; border-left: 1em solid #860; padding: 0.5em; }'
+            . '.result3{background:#f88; border-left: 1em solid #f00; padding: 0.5em; }'
+            . '.string{color:#338;}'
+            . '</style>'
+            . '<title>' . __CLASS__ . '</title>'
+            . '</head><body>'
+            . '<div id="content">'
+            . '<h1>' . __CLASS__ . ' :: client status</h1>'
+            . $sOut
+            . '</div>'
+            . '</body></html>';
+        return $sOut;
+    }
+
+}
diff --git a/public_html/appmonitor/classes/client_all_in_one.php b/public_html/appmonitor/classes/client_all_in_one.php
new file mode 100644
index 0000000000000000000000000000000000000000..b17aa09c49e0bb45388f3a6d382a66de4b644119
--- /dev/null
+++ b/public_html/appmonitor/classes/client_all_in_one.php
@@ -0,0 +1,26 @@
+<?php
+/*
+
+    MERGED APPMONITOR CLIENT :: WORK IN PROGRESS
+
+    generated Tue Jul 23 16:47:01 CEST 2024
+
+*/
+
+ class checkApacheProcesses extends appmonitorcheck { protected string $_sServerStatusUrl = 'http://localhost/server-status'; protected float $_iWarn = 50; protected float $_iError = 75; public function explain(): array { return [ 'name' => 'Plugin ApacheProcesses', 'descriptionm' => 'Check count running Apache processes', 'parameters' => [ 'url' => [ 'type' => 'string', 'required' => false, 'decsription' => 'Override https server-status page; default is http://localhost/server-status; Use it if the protocol to localhost is not http, but https or if it requires an authentication', 'default' => $this->_sServerStatusUrl, 'example' => '', ], 'warning' => [ 'type' => 'float', 'required' => false, 'decsription' => 'Limit to switch to warning (in percent)', 'default' => $this->_iWarn, 'example' => 30, ], 'error' => [ 'type' => 'float', 'required' => false, 'decsription' => 'Limit to switch to critical (in percent)', 'default' => $this->_iError, 'example' => 50, ], ], ]; } protected function _getApacheProcesses(): bool|array { $sBody = file_get_contents($this->_sServerStatusUrl); if (!$sBody) { return false; } $sRegexScoreboard = '/<pre>(.*)\<\/pre\>/U'; $aScore = []; $sStatusNobr = str_replace("\n", "", $sBody); if (preg_match_all($sRegexScoreboard, $sStatusNobr, $aTmpTable)) { $sScoreString = $aTmpTable[1][0]; $aScore['total'] = strlen($sScoreString); $aScore['free'] = substr_count($sScoreString, '.'); $aScore['waiting'] = substr_count($sScoreString, '_'); $aScore['active'] = $aScore['total'] - $aScore['free'] - $aScore['waiting']; } return $aScore; } public function getGroup(): string { return 'monitor'; } public function run(array $aParams): array { if (isset($aParams['url']) && $aParams['url']) { $this->_sServerStatusUrl = $aParams['url']; } if (isset($aParams['warning']) && (int) $aParams['warning']) { $this->_iWarn = (int) $aParams['warning']; } if (isset($aParams['error']) && (int) $aParams['error']) { $this->_iError = (int) $aParams['error']; } $aProcesses = $this->_getApacheProcesses(); $iActive = $aProcesses ? $aProcesses['active'] : false; if ($iActive === false) { $iResult = RESULT_UNKNOWN; } else { $sComment = ''; $iTotal = $aProcesses['total']; $iResult = RESULT_OK; if (($iActive / $iTotal * 100) > $this->_iWarn) { $iResult = RESULT_WARNING; $sComment = "more than warning level $this->_iWarn %"; } else { $sComment = "less than warning level $this->_iWarn %"; } if (($iActive / $iTotal * 100) > $this->_iError) { $iResult = RESULT_ERROR; $sComment = "more than error level $this->_iError %"; } } return [ $iResult, ($iActive === false ? 'Apache httpd server status is not available' : 'apache processes: ' . print_r($aProcesses, 1)) . ' ' . $sComment, ($iActive === false ? [] : [ 'type' => 'counter', 'count' => $iActive, 'visual' => 'line', ] ) ]; } } 
+ class checkCert extends appmonitorcheck { public function getGroup(): string { return 'security'; } public function run(array $aParams): array { $sUrl = $aParams["url"] ?? 'http' . ($_SERVER['HTTPS'] ? 's' : '') . '://' . $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT']; $bVerify = isset($aParams["verify"]) ? !!$aParams["verify"] : true; $iWarn = isset($aParams["warning"]) ? (int) ($aParams["warning"]) : 21; $iCrtitcal = isset($aParams["critical"]) ? (int) ($aParams["critical"]) : 5; $sMessage = "Checked url: $sUrl ... "; $certinfo = $this->_certGetInfos($sUrl, $bVerify); if (isset($certinfo['_error'])) { return [ RESULT_ERROR, $certinfo['_error'] . $sMessage ]; } $sDNS = $certinfo['extensions']['subjectAltName'] ?? false; $sHost = parse_url($sUrl, PHP_URL_HOST); if (strstr($sDNS, "DNS:$sHost") === false) { return [ RESULT_ERROR, "Wrong certificate: $sHost is not listed as DNS alias in [$sDNS]. $sMessage" ]; } $iDaysleft = round(($certinfo['validTo_time_t'] - date('U')) / 60 / 60 / 24); $sMessage .= 'Issuer: ' . $certinfo['issuer']['O'] . '; valid from: ' . date("Y-m-d H:i", $certinfo['validFrom_time_t']) . ' to ' . date("Y-m-d H:i", $certinfo['validTo_time_t']) . ' ' . ($iDaysleft ? "($iDaysleft days left)" : "expired since " . (-$iDaysleft) . " days.") ; if ($iDaysleft <= 0) { return [ RESULT_ERROR, 'Expired! ' . $sMessage ]; } if ($iDaysleft <= $iWarn) { return [ RESULT_WARNING, ($iDaysleft <= $iCrtitcal ? 'Expires very soon! ' : 'Expires soon. ' ) . $sMessage ]; } return [ RESULT_OK, 'OK. ' . ($bVerify ? 'Certificate is valid. ' : '(Verification is disabled; Check for expiration only.) ') . $sMessage ]; } } 
+ class checkDiskfree extends appmonitorcheck { public function getGroup(): string { return 'disk'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "directory,critical"); $sDirectory = $aParams["directory"]; if (!is_dir($sDirectory)) { return [ RESULT_ERROR, "directory [$sDirectory] does not exist. Maybe it is wrong or is not mounted." ]; } $iWarn = isset($aParams["warning"]) ? $this->_getSize($aParams["warning"]) : false; $iCritical = $this->_getSize($aParams["critical"]); $iSpaceLeft = disk_free_space($sDirectory); $sMessage = '[' . $sDirectory . '] has ' . $this->_getHrSize($iSpaceLeft) . ' left.'; if ($iWarn) { if ($iWarn <= $iCritical) { header('HTTP/1.0 503 Service Unavailable'); die("ERROR in a Diskfree check - warning value must be larger than critical.<pre>" . print_r($aParams, true)); } if ($iWarn < $iSpaceLeft) { return [ RESULT_OK, "$sMessage Warning level is not reached yet (still " . $this->_getHrSize($iSpaceLeft - $iWarn) . "over warning limit)." ]; } if ($iWarn > $iSpaceLeft && $iCritical < $iSpaceLeft) { return [ RESULT_WARNING, $sMessage . ' Warning level ' . $this->_getHrSize($iWarn) . ' was reached (space is ' . $this->_getHrSize($iWarn - $iSpaceLeft) . ' below warning limit; still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).' ]; } } if ($iCritical < $iSpaceLeft) { return [RESULT_OK, $sMessage . ' Minimum is not reached yet (still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).']; } else { return [RESULT_ERROR, $sMessage]; } } } 
+ class checkExec extends appmonitorcheck { public function getGroup() { return 'service'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "command"); $_sCmd = $aParams['command']; $_bShowOutput = isset($aParams['output']) ? !!$aParams['output'] : true; $_aRcOK = isset($aParams['exitOK']) ? $aParams['exitOK'] : []; $_aRcWarning = isset($aParams['exitWarn']) ? $aParams['exitWarn'] : []; $_aRcCritical = isset($aParams['exitCritical']) ? $aParams['exitCritical'] : []; $_sMode = 'default'; if (count($_aRcOK) + count($_aRcWarning) + count($_aRcCritical)) { $_sMode = 'exitcode'; } exec($_sCmd, $aOutput, $iRc); $_sOut = $_bShowOutput ? '<br>' . implode("<br>", $aOutput) : ''; switch ($_sMode) { case "default": if ($iRc) { return [ RESULT_ERROR, 'command failed with exitcode ' . $iRc . ': [' . $_sCmd . ']' . $_sOut ]; } else { return [ RESULT_OK, "OK [$_sCmd] $_sOut" ]; } ; ; case "exitcode": if (in_array($iRc, $_aRcCritical)) { return [ RESULT_ERROR, "Critical exitcode $iRc detected: [$_sCmd] $_sOut" ]; } if (in_array($iRc, $_aRcWarning)) { return [ RESULT_WARNING, "Warning exitcode $iRc detected: [$_sCmd] $_sOut" ]; } if ($iRc == 0 || in_array($iRc, $_aRcOK)) { return [ RESULT_OK, "OK exitcode $iRc detected: [$_sCmd] $_sOut" ]; } return [ RESULT_UNKNOWN, "UNKNOWN - unhandled exitcode $iRc detected: [$_sCmd] $_sOut" ]; case "search": return [ RESULT_UNKNOWN, "UNKNOWN method [$_sMode] - is not implemented yet." ]; ; default: return [ RESULT_UNKNOWN, 'UNKNOWN mode [' . htmlentities($_sMode) . '].' ]; } } } 
+ class checkFile extends appmonitorcheck { public function getGroup(array $aParams = []): string { $sReturn = 'file'; if (isset($aParams['dir'])) { $sReturn = 'folder'; } foreach (['exists', 'executable', 'readable', 'writable'] as $sFlag) { if (isset($aParams[$sFlag]) && !$aParams[$sFlag]) { $sReturn = 'deny'; } } return $sReturn; } public function run(array $aParams): array { $aOK = []; $aErrors = []; $this->_checkArrayKeys($aParams, "filename"); $sFile = $aParams["filename"]; if (isset($aParams['exists'])) { $sMyflag = 'exists=' . ($aParams['exists'] ? 'yes' : 'no'); if (file_exists($sFile) && $aParams['exists']) { $aOK[] = $sMyflag; } else { $aErrors[] = $sMyflag; } } foreach (['dir', 'executable', 'file', 'link', 'readable', 'writable'] as $sFiletest) { if (isset($aParams[$sFiletest])) { $sTestCmd = 'return is_' . $sFiletest . '("' . $sFile . '");'; if (eval ($sTestCmd) && $aParams[$sFiletest]) { $aOK[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no'); } else { $aErrors[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no'); } } } $sMessage = (count($aOK) ? ' flags OK: ' . implode('|', $aOK) : '') . ' ' . (count($aErrors) ? ' flags FAILED: ' . implode('|', $aErrors) : '') ; if (count($aErrors)) { return [ RESULT_ERROR, "file test [$sFile] $sMessage" ]; } else { return [ RESULT_OK, "file test [$sFile] $sMessage" ]; } } } 
+ class checkHello extends appmonitorcheck { public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "message"); return [ RESULT_OK, 'Hello world! My message is: ' . $aParams['message'] ]; } } 
+ class checkHttpContent extends appmonitorcheck { public function getGroup(array $aParams=[]): string { $sReturn = 'service'; if (isset($aParams['status']) && $aParams['status'] > 300 && $aParams['status'] < 500) { $sReturn = 'deny'; } return $sReturn; } public function run(array $aParams) { $this->_checkArrayKeys($aParams, "url"); if (!function_exists("curl_init")) { header('HTTP/1.0 503 Service Unavailable'); die("ERROR: PHP CURL module is not installed."); } $bShowContent = (isset($aParams["content"]) && $aParams["content"]) ? true : false; $ch = curl_init($aParams["url"]); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_NOBODY, isset($aParams["headeronly"]) && $aParams["headeronly"]); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, isset($aParams["follow"]) && $aParams["follow"]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, isset($aParams["sslverify"]) ? !!$aParams["sslverify"] : 1); curl_setopt($ch, CURLOPT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp); if (isset($aParams["userpwd"])) { curl_setopt($ch, CURLOPT_USERPWD, $aParams["userpwd"]); } $res = curl_exec($ch); if (!$res) { $iErrorCode = curl_errno($ch); $sErrorMsg = curl_error($ch); curl_close($ch); return [ RESULT_ERROR, 'ERROR: failed to fetch ' . $aParams["url"] . ' - curl error #' . $iErrorCode . ': ' . $sErrorMsg ]; } $sOut = ''; $bError = false; $aInfos = curl_getinfo($ch); curl_close($ch); $aTmp = explode("\r\n\r\n", $res, 2); $sHttpHeader = $aTmp[0]; $sHttpBody = $aTmp[1] ?? false; $sOut .= "Http status: " . $aInfos['http_code'] . " - "; if (isset($aParams["status"])) { if ($aInfos['http_code'] === $aParams["status"]) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } else { if ($aInfos['http_code'] >= 400) { $sOut .= "Error page detected<br>"; $bError = true; } else { $sOut .= "request successful<br>"; } } if (isset($aParams["headercontains"]) && $aParams["headercontains"]) { $sOut .= "Http header contains &quot;" . $aParams["headercontains"] . "&quot; - "; if (!strstr($sHttpHeader, $aParams["headercontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["headernotcontains"]) && $aParams["headernotcontains"]) { $sOut .= "Http header does not contain &quot;" . $aParams["headernotcontains"] . "&quot; - "; if (strstr($sHttpHeader, $aParams["headernotcontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["headerregex"]) && $aParams["headerregex"]) { $sOut .= "Http header regex test &quot;" . $aParams["headerregex"] . "&quot; - "; try { $bRegex = preg_match($aParams["headerregex"], $sHttpHeader); if ($bRegex) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } catch (Exception $e) { $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>'; $bError = true; } } if (isset($aParams["bodycontains"]) && $aParams["bodycontains"]) { $sOut .= "Http body contains &quot;" . $aParams["bodycontains"] . "&quot; - "; if (!strstr($sHttpBody, $aParams["bodycontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["bodynotcontains"]) && $aParams["bodynotcontains"]) { $sOut .= "Http body does not contain &quot;" . $aParams["bodynotcontains"] . "&quot; - "; if (strstr($sHttpBody, $aParams["bodynotcontains"]) === false) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } if (isset($aParams["bodyregex"]) && $aParams["bodyregex"]) { $sOut .= "Http body regex test &quot;" . $aParams["bodyregex"] . "&quot; - "; try { $bRegex = preg_match($aParams["bodyregex"], $sHttpBody); if ($bRegex) { $sOut .= "compare OK<br>"; } else { $sOut .= "compare failed<br>"; $bError = true; } } catch (Exception $e) { $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>'; $bError = true; } } if (!$bError) { return [ RESULT_OK, 'OK: http check "' . $aParams["url"] . '".<br>' . $sOut ]; } else { return [ RESULT_ERROR, 'ERROR: http check "' . $aParams["url"] . '".<br>' . $sOut ]; } } } 
+ class checkLoadmeter extends appmonitorcheck { public function getGroup(): string { return 'monitor'; } protected function _getLoad(): float { if (function_exists('sys_getloadavg')) { $load = sys_getloadavg(); return $load[0]; } else { if (class_exists('COM')) { $wmi = new COM('WinMgmts:\\\\.'); $cpus = $wmi->InstancesOf('Win32_Processor'); $load = 0; if (version_compare('4.50.0', PHP_VERSION) == 1) { while ($cpu = $cpus->Next()) { $load += $cpu->LoadPercentage; } } else { foreach ($cpus as $cpu) { $load += $cpu->LoadPercentage; } } return $load; } return false; } } public function run(array $aParams): array { $fLoad = $this->_getLoad(); if ($fLoad === false) { $iResult = RESULT_UNKNOWN; } else { $iResult = RESULT_OK; if (isset($aParams['warning']) && $aParams['warning'] && $fLoad > $aParams['warning']) { $iResult = RESULT_WARNING; } if (isset($aParams['error']) && $aParams['error'] && $fLoad > $aParams['error']) { $iResult = RESULT_ERROR; } } return [ $iResult, ($fLoad === false ? 'load value is not available' : 'current load is: ' . $fLoad), ($fLoad === false ? [] : [ 'type' => 'counter', 'count' => $fLoad, 'visual' => 'line', ] ) ] ; } } 
+ class checkMysqlConnect extends appmonitorcheck { public function getGroup(): string { return 'database'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "server,user,password,db"); $mysqli = mysqli_init(); if (!$mysqli) { return [RESULT_ERROR, 'ERROR: mysqli_init failed.']; } if (!$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp)) { return [RESULT_ERROR, 'ERROR: setting mysqli_options failed.']; } $db = (isset($aParams["port"]) && $aParams["port"]) ? $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"], $aParams["port"]) : $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"]) ; if ($db) { $mysqli->close(); return [RESULT_OK, "OK: Mysql database " . $aParams["db"] . " was connected"]; } else { return [ RESULT_ERROR, "ERROR: Mysql database " . $aParams["db"] . " was not connected. Error " . mysqli_connect_errno() . ": " . mysqli_connect_error() ]; } } } 
+ class checkPdoConnect extends appmonitorcheck { public function getGroup(): string { return 'database'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "connect,user,password"); try { $db = new PDO( $aParams['connect'], $aParams['user'], $aParams['password'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, ] ); $db = null; return [RESULT_OK, "OK: Database was connected with PDO " . $aParams['connect']]; } catch (PDOException $e) { return [RESULT_ERROR, "ERROR: Database was not connected " . $aParams['connect'] . " was not connected. Error " . $e->getMessage()]; } } } 
+ class checkPhpmodules extends appmonitorcheck { public function getGroup(): string { return 'service'; } public function run(array $aParams): array { $sOut = ''; $bHasError = false; $bHasWarning = false; $aAllMods = get_loaded_extensions(false); if (isset($aParams['required']) && count($aParams['required'])) { $sOut .= 'Required: '; foreach ($aParams['required'] as $sMod) { $sOut .= $sMod . '='; if (!array_search($sMod, $aAllMods) === false) { $sOut .= 'OK;'; } else { $bHasError = true; $sOut .= 'MISS;'; } } } if (isset($aParams['optional']) && count($aParams['optional'])) { $sOut .= ($sOut ? '|' : '') . 'Optional: '; foreach ($aParams['optional'] as $sMod) { $sOut .= $sMod . '='; if (!array_search($sMod, $aAllMods) === false) { $sOut .= 'OK;'; } else { $bHasWarning = true; $sOut .= 'MISS;'; } } } if ($bHasError) { return [RESULT_ERROR, "ERROR: " . $sOut]; } if ($bHasWarning) { return [RESULT_WARNING, "WARNING: " . $sOut]; } return [RESULT_OK, "OK: " . $sOut]; } } 
+ class checkPing extends appmonitorcheck { public function getGroup(): string { return 'network'; } public function run(array $aParams): array { $sHost = $aParams['host'] ?? '127.0.0.1'; $sParamCount = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? "n" : "c"; $iRepeat = 1; $sCommand = "ping -$sParamCount $iRepeat $sHost 2>&1"; exec($sCommand, $aOut, $iRc); $sOut = implode("\n", $aOut); if ($iRc > 0) { return [RESULT_ERROR, "ERROR: ping to $sHost failed.\n" . $sOut]; } return [RESULT_OK, "OK: ping to $sHost\n" . $sOut]; } } 
+ class checkPortTcp extends appmonitorcheck { public function getGroup(): string { return 'network'; } public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "port"); $sHost = $aParams['host'] ?? '127.0.0.1'; $iPort = (int) $aParams['port']; if (!function_exists('socket_create')) { return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform tcp test. The socket module is not enabled in the php installation."]; } $socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($socket === false) { return [RESULT_UNKNOWN, "ERROR: $sHost:$iPort was not checked. socket_create() failed: " . socket_strerror(socket_last_error())]; } socket_set_option( $socket, SOL_SOCKET, SO_SNDTIMEO, [ "sec" => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, "usec" => 0 ] ); $result = @socket_connect($socket, $sHost, $iPort); if ($result === false) { $aResult = [RESULT_ERROR, "ERROR: $sHost:$iPort failed. " . socket_strerror(socket_last_error($socket))]; socket_close($socket); return $aResult; } else { socket_close($socket); return [RESULT_OK, "OK: $sHost:$iPort was connected."]; } } } 
+ class checkSimple extends appmonitorcheck { public function run(array $aParams): array { $this->_checkArrayKeys($aParams, "result,value"); $aData = []; foreach (['type', 'count', 'visual'] as $sMyKey) { if (isset($aParams[$sMyKey])) { $aData[$sMyKey] = $aParams[$sMyKey]; } } return [ $aParams["result"], $aParams["value"], count($aData) ? $aData : false ]; } } 
+ class checkSqliteConnect extends appmonitorcheck { public function getGroup() { return 'database'; } public function run($aParams): array { $this->_checkArrayKeys($aParams, "db"); if (!file_exists($aParams["db"])) { return [ RESULT_ERROR, "ERROR: Sqlite database file " . $aParams["db"] . " does not exist." ]; } if (!isset($aParams['user'])) { $aParams['user'] = ''; } if (!isset($aParams['password'])) { $aParams['password'] = ''; } try { $o = new PDO( "sqlite:" . $aParams["db"], $aParams['user'], $aParams['password'], [ PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, ] ); return [ RESULT_OK, "OK: Sqlite database " . $aParams["db"] . " was connected" ]; } catch (Exception $e) { return [ RESULT_ERROR, "ERROR: Sqlite database " . $aParams["db"] . " was not connected. " . $e->getMessage() ]; } } } 
+ if (!defined('RESULT_OK')) { define("RESULT_OK", 0); define("RESULT_UNKNOWN", 1); define("RESULT_WARNING", 2); define("RESULT_ERROR", 3); } class appmonitorcheck { protected float $_iStart = 0; protected array $_aConfig = []; protected array $_aData = []; protected array $_units = ['B', 'KB', 'MB', 'GB', 'TB']; protected int $_iTimeoutTcp = 5; protected string $_sPluginDir = __DIR__ . '/../plugins'; public function __construct() { } protected function _createDefaultMetadata(): bool { $this->_aData = [ "name" => $this->_aConfig["name"], "description" => $this->_aConfig["description"], "group" => isset($this->_aConfig["group"]) ? $this->_aConfig["group"] : false, "parent" => isset($this->_aConfig["parent"]) ? $this->_aConfig["parent"] : false, "result" => RESULT_UNKNOWN, "value" => false, "type" => false, "time" => false, ]; return true; } protected function _setResult(int $iResult): true { $this->_aData["result"] = (int) $iResult; return true; } protected function _setOutput(string $s): bool { $this->_aData["value"] = $s; return true; } protected function _setCounter(array $aParams): bool { if (is_array($aParams) && count($aParams)) { foreach (['type', 'count', 'visual'] as $sMyKey) { if (isset($aParams[$sMyKey])) { $this->_aData[$sMyKey] = $aParams[$sMyKey]; } } } return true; } protected function _setReturn(int $iResult, string $s, array $aCounter = []) { $this->_setResult($iResult); $this->_setOutput($s); $this->_setCounter($aCounter); return true; } protected function _checkArrayKeys($aConfig, $sKeyList) { foreach (explode(",", $sKeyList) as $sKey) { if (!isset($aConfig[$sKey])) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - array of check parameters requires the keys [$sKeyList] - but key <code>$sKey</code> was not found in config array." . "<pre>" . print_r($aConfig, true) . '</pre>' ); } if (is_null($aConfig[$sKey])) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - key <code>$sKey</code> is empty in config array" . "<pre>" . print_r($aConfig, true) . '</pre>' ); } } return true; } public function makeCheck(array $aConfig): array { $this->_iStart = microtime(true); $this->_checkArrayKeys($aConfig, "name,description,check"); $this->_checkArrayKeys($aConfig["check"], "function"); $this->_aConfig = $aConfig; $this->_createDefaultMetadata(); $sCheck = preg_replace('/[^a-zA-Z0-9]/', '', $this->_aConfig["check"]["function"]); $aParams = $this->_aConfig["check"]["params"] ?? []; $sPluginFile = strtolower($this->_sPluginDir . '/checks/' . $sCheck . '.php'); $sCheckClass = 'check' . $sCheck; if (!class_exists($sCheckClass)) { if (file_exists($sPluginFile)) { } } if (!class_exists($sCheckClass)) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - check class not found: <code>$sCheckClass</code>" . "<pre>" . print_r($aConfig, true) . '</pre>' . "<h2>Known checks</h2>\n" . print_r($this->listChecks(), 1) ); } $oPlugin = new $sCheckClass; $aResponse = $oPlugin->run($aParams); if (!is_array($aResponse)) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - plugin : $sCheck does not responses an array" . "<pre>INPUT " . print_r($aConfig, true) . '</pre>' . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>' ); } if (count($aResponse) < 2) { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " - plugin : $sCheck does not responses the minimum of 2 array values" . "<pre>INPUT " . print_r($aConfig, true) . '</pre>' . "<pre>RESPONSE " . print_r($aResponse, true) . '</pre>' ); } if (!isset($aResponse[2]) || !$aResponse[2]) { $aResponse[2] = []; } $this->_setReturn($aResponse[0], $aResponse[1], $aResponse[2]); if (!$this->_aData['group'] && method_exists($oPlugin, "getGroup")) { $this->_aData['group'] = $oPlugin->getGroup($aParams); } $this->_aData['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; return $this->respond(); } public function listChecks(): array { $aReturn = []; $class = new ReflectionClass($this); foreach ($class->getMethods(ReflectionMethod::IS_PROTECTED) as $oReflectionMethod) { if (strpos($oReflectionMethod->name, "check") === 0) { $aReturn[(string) $oReflectionMethod->name] = 1; } } foreach (glob($this->_sPluginDir . '/checks/*.php') as $sPluginFile) { $aReturn[str_replace('.php', '', basename($sPluginFile))] = 1; } ksort($aReturn); return array_keys($aReturn); } public function respond() { return $this->_aData; } protected function _certGetInfos(string $sUrl, bool $bVerifyCert): array { $iTimeout = 10; $aUrldata = parse_url($sUrl); $sHost = isset($aUrldata['host']) ? $aUrldata['host'] : false; $iPort = isset($aUrldata['port']) ? $aUrldata['port'] : ((isset($aUrldata['scheme']) && $aUrldata['scheme'] === 'https') ? 443 : false); $aSsl = ['capture_peer_cert' => true]; if ($bVerifyCert) { $aSsl['verify_peer'] = false; $aSsl['verify_peer_name'] = false; } ; $get = stream_context_create(['ssl' => $aSsl]); if (!$get) { return ['_error' => 'Error: Cannot create stream_context']; } $errno = -1; $errstr = "stream_socket_client failed."; $read = stream_socket_client("ssl://$sHost:$iPort", $errno, $errstr, $iTimeout, STREAM_CLIENT_CONNECT, $get); if (!$read) { return ['_error' => "Error $errno: $errstr; cannot create stream_socket_client with given stream_context to ssl://$sHost:$iPort; you can try to set the flag [verify] to false to check expiration date only."]; } $cert = stream_context_get_params($read); if (!$cert) { return ['_error' => "Error: socket was connected to ssl://$sHost:$iPort - but I cannot read certificate infos with stream_context_get_params "]; } return openssl_x509_parse($cert['options']['ssl']['peer_certificate']); } protected function _getHrSize(int $size): string { $power = $size > 0 ? floor(log($size, 1024)) : 0; return number_format($size / pow(1024, $power), 2, '.', ',') . ' ' . $this->_units[$power]; } protected function _getSize(string $sValue): int { if (is_int($sValue)) { return $sValue; } $power = 0; foreach ($this->_units as $sUnit) { if (preg_match('/^[0-9\.\ ]*' . $sUnit . '/', $sValue)) { $i = preg_replace('/([0-9\.]*).*/', '$1', $sValue); $iReal = $i * pow(1024, $power); return $iReal; } $power++; } header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . '<h2>Details</h2>' . __METHOD__ . " ERROR in space value parameter - there is no size unit in [$sValue] - allowed size units are " . implode('|', $this->_units) ); } } 
+if (!class_exists('appmonitorcheck')) { } class appmonitor { protected string $_sVersion = 'php-client-v0.137'; protected int $_iDefaultTtl = 300; protected int $_iMaxResult = -1; protected array $_aMeta = []; protected array $_aChecks = []; protected float $_iStart = 0; public function __construct() { $this->_createDefaultMetadata(); } protected function _createDefaultMetadata(): bool { $this->_iStart = microtime(true); $this->_aMeta = [ "host" => false, "website" => false, "ttl" => false, "result" => false, "time" => false, "version" => $this->_sVersion, ]; $this->setHost(); $this->setWebsite(); $this->setTTL(); return true; } public function setHost(string $s = ''): bool { if (!$s) { $s = php_uname("n"); } if (!$s) { return false; } $this->_aMeta["host"] = $s; return true; } public function setWebsite($sWebsite = ''): bool { if (!$sWebsite && isset($_SERVER["HTTP_HOST"])) { $sWebsite = $_SERVER["HTTP_HOST"]; } if (!$sWebsite) { return false; } $this->_aMeta["website"] = $sWebsite; return true; } public function setTTL($iTTl = 0) { if ($iTTl == 0) { $iTTl = $this->_iDefaultTtl; } return $this->_aMeta["ttl"] = $iTTl; } public function setResult(int $iResult = -1): bool { if ($iResult === -1) { $iResult = $this->_iMaxResult; } $this->_aMeta["result"] = $iResult; return true; } public function addCheck($aJob = []): bool { $oCheck = new appmonitorcheck(); $aCheck = $oCheck->makecheck($aJob); $iMyResult = isset($aJob['worstresult']) ? min($aCheck["result"], $aJob['worstresult']) : $aCheck["result"] ; if (!$this->_iMaxResult || $iMyResult > $this->_iMaxResult) { $this->_iMaxResult = $iMyResult; } $this->_aChecks[] = $aCheck; return true; } protected function _addNotification(string $sType, string $sValue, string $sKey = ''): bool { $sTypeCleaned = preg_replace('/[^a-z]/', '', strtolower($sType)); if (!isset($this->_aMeta['notifications'])) { $this->_aMeta['notifications'] = []; } if (!isset($this->_aMeta['notifications'][$sTypeCleaned])) { $this->_aMeta['notifications'][$sTypeCleaned] = []; } if ($sKey) { $this->_aMeta['notifications'][$sTypeCleaned][$sKey] = $sValue; } else { $this->_aMeta['notifications'][$sTypeCleaned][] = $sValue; } return true; } public function addEmail(string $sEmailAddress) { return $this->_addNotification('email', $sEmailAddress); } public function addSlackWebhook(string $sLabel, string $sSlackWebhookUrl): bool { return $this->_addNotification('slack', $sSlackWebhookUrl, $sLabel); } public function addTag(string $sTag): bool { if (!isset($this->_aMeta['tags'])) { $this->_aMeta['tags'] = []; } $this->_aMeta['tags'][] = str_replace(' ', '_', $sTag); return true; } public function checkIp(array $aAllowedIps = []): bool { if (!isset($_SERVER['REMOTE_ADDR']) || !count($aAllowedIps)) { return true; } $sIP = $_SERVER['REMOTE_ADDR']; foreach ($aAllowedIps as $sIp2Check) { if (strpos($sIP, $sIp2Check) === 0) { return true; } } header('HTTP/1.0 403 Forbidden'); die('ERROR: Your ip address [' . $sIP . '] has no access.'); } public function checkToken(string $sVarname, string $sToken): bool { if (!isset($_GET)) { return true; } if (isset($_GET[$sVarname]) && $_GET[$sVarname] === $sToken) { return true; } header('HTTP/1.0 403 Forbidden'); die('ERROR: A token is required.'); } public function listChecks(): array { $oCheck = new appmonitorcheck(); return $oCheck->listChecks(); } protected function _checkData(): bool { $aErrors = []; if (!count($this->_aChecks)) { $aErrors[] = "No checks have been defined."; } if ($this->_aMeta["result"] === false) { $aErrors[] = "method setResult was not used to set a final result for all checks."; } if (count($aErrors)) { $this->abort( '<h2>Error: client check is not complete</h2><p>Found errors:</p><ol><li>' . implode('<li>', $aErrors) . '</ol><br><br>' ); } return true; } public function abort(string $sMessage): void { header('HTTP/1.0 503 Service Unavailable'); die('<h1>503 Service Unavailable</h1>' . $sMessage); } public function getResults(): array { return [ "meta" => $this->_aMeta, "checks" => $this->_aChecks, ]; } public function render(): string { $this->_checkData(); $this->_aMeta['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; $sOut=json_encode($this->getResults()); header('Content-type: application/json'); header('Cache-Control: cache'); header('max-age: ' . $this->_aMeta["ttl"]); echo $sOut; return $sOut; } public function renderHtmloutput(string $sJson): string { header('Content-type: text/html'); header('Cache-Control: cache'); header('max-age: ' . $this->_aMeta["ttl"]); $aMsg = [ 0 => "OK", 1 => "UNKNOWN", 2 => "WARNING", 3 => "ERROR" ]; $aData = json_decode($sJson, 1); $sOut = ''; $sOut .= '' . '<h2>Metadata</h2>' . '<div class="meta' . (isset($aData['meta']['result']) ? ' result' . $aData['meta']['result'] : '') . '">' . 'Status: ' . (isset($aData['meta']['result']) ? $aMsg[$aData['meta']['result']] : '?') . '<br>' . '</div>' . 'Host: ' . (isset($aData['meta']['host']) ? '<span class="string">' . $aData['meta']['host'] . '</span>' : '?') . '<br>' . 'Website: ' . (isset($aData['meta']['website']) ? '<span class="string">' . $aData['meta']['website'] . '</span>' : '?') . '<br>' . 'Execution time: ' . (isset($aData['meta']['time']) ? '<span class="float">' . $aData['meta']['time'] . '</span>' : '?') . '<br>' . 'Client: ' . (isset($aData['meta']['version']) ? '<span class="string">' . $aData['meta']['version'] . '</span>' : '?') . '<br>' . '<h2>Checks</h2>' ; if (isset($aData['checks'][0]) && count($aData['checks'])) { foreach ($aData['checks'] as $aCheck) { $sOut .= '' . '<span class="result' . $aCheck['result'] . '"> <strong>' . $aCheck['name'] . '</strong></span> <br>' . '<div class="check">' . '<div class="description">' . $aCheck['description'] . '<br>' . $aCheck['value'] . '<br>' . '</div>' . 'Execution time: <span class="float">' . (isset($aCheck['time']) ? $aCheck['time'] : ' - ') . '</span><br>' . 'Group: <span class="string">' . (isset($aCheck['group']) ? $aCheck['group'] : '-') . '</span><br>' . 'parent: <span class="string">' . (isset($aCheck['parent']) ? $aCheck['parent'] : '-') . '</span><br>' . 'Status: ' . $aMsg[$aCheck['result']] . '<br>' . '</div>' ; } } $sOut .= '<h2>List of farbcodes</h2>'; foreach ($aMsg as $i => $sText) { $sOut .= '<span class="result' . $i . '">' . $sText . '</span> '; } $sRaw=json_encode($aData, JSON_PRETTY_PRINT); $sRaw = preg_replace('/:\ \"(.*)\"/U', ': "<span class="string">$1</span>"', $sRaw); $sRaw = preg_replace('/:\ ([0-9]*)/', ': <span class="int">$1</span>', $sRaw); $sRaw = preg_replace('/\"(.*)\":/U', '"<span class="key">$1</span>":', $sRaw); $sOut .= '<h2>Raw result data</h2><pre id="raw">' . $sRaw . '</pre>'; $sOut = '<!DOCTYPE html><html><head>' . '<style>' . 'body{background:#eee; color:#444; font-family: verdana,arial; margin: 0; }' . 'body>div#content{background: #fff; border-radius: 2em; border: 4px solid #abc; box-shadow: 0.5em 0.5em 2em #aaa; margin: 2em 10%; padding: 2em;}' . 'h1{color:#346; margin: 0;}' . 'h2{color:#569; margin-top: 2em;}' . 'pre{background:#f4f4f8; padding: 1em; overflow-x:auto; }' . '#raw .key{color:#808;}' . '#raw .int{color:#3a3; font-weight: bold;}' . '#raw .string{color:#66e;}' . '.check{border: 1px solid #ccc; padding: 0.4em; margin-bottom: 2em;}' . '.description{font-style: italic; padding: 0.4em 1em;}' . '.float{color:#080;}' . '.meta{margin-bottom: 1em;}' . '.result0{background:#aca; border-left: 1em solid #080; padding: 0.5em; }' . '.result1{background:#ccc; border-left: 1em solid #aaa; padding: 0.5em; }' . '.result2{background:#fc9; border-left: 1em solid #860; padding: 0.5em; }' . '.result3{background:#f88; border-left: 1em solid #f00; padding: 0.5em; }' . '.string{color:#338;}' . '</style>' . '<title>' . __CLASS__ . '</title>' . '</head><body>' . '<div id="content">' . '<h1>' . __CLASS__ . ' :: client status</h1>' . $sOut . '</div>' . '</body></html>'; return $sOut; } } 
\ No newline at end of file
diff --git a/public_html/appmonitor/general_include.php b/public_html/appmonitor/general_include.php
new file mode 100644
index 0000000000000000000000000000000000000000..a6ffb5eccd18e99a69582845157caf50a8727981
--- /dev/null
+++ b/public_html/appmonitor/general_include.php
@@ -0,0 +1,32 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECK  ::  GENERAL INCLUDE
+ * ______________________________________________________________________
+ * 
+ * @author: Axel Hahn
+ * ----------------------------------------------------------------------
+ * 2025-01-06  initial version
+ */
+
+
+// ----------------------------------------------------------------------
+// set a tag with phase
+// ----------------------------------------------------------------------
+$sHost=$_SERVER['HTTP_HOST'];
+$sHost2=php_uname("n");   
+$sMyPhase='live';
+
+foreach (array('dev', 'preview', 'stage', 'demo') as $sPhase){
+    if(
+        strstr($sHost.'.', $sPhase)!==false
+        || strstr($sHost.'-', $sPhase)!==false
+        || strstr($sHost2.'.', $sPhase)!==false
+        || strstr($sHost2.'-', $sPhase)!==false
+    ) {
+        $sMyPhase=$sPhase;
+        break;
+    }
+}
+$oMonitor->addTag($sMyPhase);
+
diff --git a/public_html/appmonitor/git_update_appmonitor.sh b/public_html/appmonitor/git_update_appmonitor.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0426661154b1f446ce991eb2b4ad1a33894067d5
--- /dev/null
+++ b/public_html/appmonitor/git_update_appmonitor.sh
@@ -0,0 +1,272 @@
+#!/bin/bash
+# ======================================================================
+#
+#   
+#    _____ _____ __                   _____         _ _           
+#   |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+#   |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+#   |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+#                            |_| |_|                              
+#                             _ _         _                                            
+#                         ___| |_|___ ___| |_                                          
+#                        |  _| | | -_|   |  _|                                         
+#                        |___|_|_|___|_|_|_|   
+#                                                                 
+#
+#                         INSTALLER + UPDATER
+#
+# This script will install or update the appmonitor client only.
+#
+# Below the document root of a website create a new directory, 
+# i.e. [webroot]/appmonitor/ and copy this script there.
+# Change the directory "cd [webroot]/appmonitor/" and execute it.
+#
+# ----------------------------------------------------------------------
+# requires git, rsync
+# ----------------------------------------------------------------------
+# 2022-04-11  0.1  <axel.hahn@iml.unibe.ch>  first lines
+# 2022-04-12  0.2  <axel.hahn@iml.unibe.ch>  add help; exclude unneeded files
+# 2022-05-03  0.3  <axel.hahn@iml.unibe.ch>  create general_include.php
+# 2024-07-25  0.4  <axel.hahn@iml.unibe.ch>  update quoting and comments
+# 2024-07-31  0.5  <axel.hahn@iml.unibe.ch>  Show more helpful information; wait on 1st install; added param -n
+# 2024-12-23  0.6  <axel.hahn@iml.unibe.ch>  remove which command. Maybe it is not installed on a shared hoster.
+# 2024-12-26  0.7  <axel.hahn@iml.unibe.ch>  rsync test with --version instead of -V (for compatibility with older versions)
+# 2025-01-06  0.8  <axel.hahn@iml.unibe.ch>  git test with --version instead of -v (for compatibility with older versions)
+# ======================================================================
+
+# ----------------------------------------------------------------------
+# CONFIG
+# ----------------------------------------------------------------------
+
+readonly git_repo_url="https://github.com/iml-it/appmonitor.git"
+readonly docs_url="https://os-docs.iml.unibe.ch/appmonitor/PHP_client/index.html"
+readonly line="______________________________________________________________________________"
+readonly version="0.8"
+
+git_target=/tmp/git_data__appmonitor
+client_from="${git_target}/public_html/client"
+client_to="."
+isUpdate=0
+wait=1
+
+
+cd "$( dirname "$0" )" || exit 1
+
+# ----------------------------------------------------------------------
+# FUNCTIONS
+# ----------------------------------------------------------------------
+
+# Create a missing file from sample file
+#
+# global $client_from  source dir with git repo data
+# global $client_to    target dir
+#
+# param  string  source file (containing .sample); relative to $client_from
+function _fileupdate(){
+    local _myfile=$1
+    local _newfile=${_myfile//.sample/}
+    echo -n "Update $client_from/$_myfile --> $client_to/$_newfile ... "
+    
+    if [ ! -f "$client_to/$_newfile" ]; then
+        echo -n "copy ... "
+        cp "$client_from/$_myfile" "$client_to/$_newfile" || exit 2
+        echo "OK"
+    else
+        echo "already exists - SKIP "
+    fi
+
+}
+
+# get data from a repo with git clone or git pull
+# param string  url of public .git repo
+# param string  local directory where to clone it
+function _gitUpdate(){
+    local _url=$1
+    local _dirgit=$2
+    local _rc=0
+    if [ -d "$_dirgit" ]; then
+        cd "$_dirgit" || exit 1
+        _logBefore=$( git log -1 );
+        echo "Update local data from repo... with git pull "
+        git pull
+        _logAfter=$( git log -1 ); 
+        if [ "$_logBefore" != "$_logAfter" ]; then
+            _rc=1
+        fi
+        cd - >/dev/null || exit 1
+    else
+        echo "Cloning..."
+        git clone "$_url" "$_dirgit"
+        _rc=$?
+    fi
+    return $_rc
+}
+
+
+# ----------------------------------------------------------------------
+# MAIN
+# ----------------------------------------------------------------------
+
+cat <<ENDOFHEADER
+$line
+
+    IML Appmonitor client   ::   installer + updater  v$version
+$line
+
+
+ENDOFHEADER
+
+case "$1" in
+    -h|--help)
+        cat <<ENDOFHELP
+    The IML Appmonitor is free software.
+
+        Source: https://github.com/iml-it/appmonitor
+        Docs: https://os-docs.iml.unibe.ch/appmonitor
+        License: GNU GPL 3.0
+
+    This is a helper script to get the files of the IML Appmonitor
+    client part only.
+
+    Below the document root of a website create a new directory, 
+    i.e. [webroot]/appmonitor/ and copy this script there.
+
+    This script clones and updates the repository in the /tmp 
+    directory and syncs the client files of it to a given directory.
+
+    In the first run it works like an installer.
+    On additional runs it updates the files.
+
+    USAGE:
+        $0 [OPTIONS] [TARGET]
+
+    OPTIONS:
+        -h|--help
+            Show this help and exit
+        -n|--nowait
+            Do not wait for RETURN on 1st installation.
+            Use it for an unattended installation.
+
+    PARAMETERS:
+        TARGET 
+            optional target path for the client files
+            default target is "." (current directory)
+
+ENDOFHELP
+        exit 0
+        ;;
+    -n|--nowait)
+        wait=0
+        ;;
+    *)
+        if test -n "$1" 
+            then
+            if  ! test -d "$1"
+            then 
+                echo "ERROR: target dir [$1] does not exist."
+                exit 1
+            fi
+            echo "set target to $1"
+            client_to="$1"
+        fi
+esac
+
+# which rsync >/dev/null || exit 1
+# which git >/dev/null || exit 1
+
+rsync --version >/dev/null || exit 1
+git --version >/dev/null || exit 1
+
+test -f general_include.php && isUpdate=1
+
+if [ $isUpdate -eq 0 ]; then
+    cat <<WELCOME
+    Welcome to the Appmonitor client installation!
+
+
+    This is a helper script to get the client files of the IML Appmonitor.
+    They will be installed into the directory "$client_to" $( test "$client_to" = "." && (echo; echo -n "    "; pwd) )
+
+        If this is not correct, press Ctrl + C to abort and use a
+        parameter to set another target directory.
+
+        "$( basename "$0" ) -h" shows a help and more options.
+
+
+WELCOME
+    if [ $wait -eq 1 ]; then
+        echo -n "    RETURN to continue ... "
+        read -r
+    fi
+else
+    echo "Updating local files ..."
+fi
+echo
+
+echo $line
+echo ">>> #1 of 3 >>> update local git data"
+echo
+echo "URL $git_repo_url"
+echo "TO  $git_target"
+if ! _gitUpdate "$git_repo_url" "$git_target"
+then 
+    echo ERROR occured :-/
+    exit 1
+fi
+echo
+
+
+echo $line
+echo ">>> #2 of 3 >>> Sync files of Appmonitor client"
+echo
+echo "FROM $client_from/*" 
+echo "TO   $client_to"
+rsync -rav \
+    --exclude "build" \
+    --exclude "*.sample.*" \
+    --exclude "example.json" \
+    --exclude "check-appmonitor-server.php" \
+    --exclude "local.php" \
+    --exclude "git_update_appmonitor.sh" \
+    $client_from/* "$client_to"
+echo
+
+_fileupdate general_include.sample.php
+
+echo $line
+echo ">>> #3 of 3 >>> Diff"
+echo
+diff --color -r "$client_from" "$client_to"
+echo
+
+if [ $isUpdate -eq 0 ]; then
+    _fileupdate index.sample.php
+    cat <<INTRODUCTION
+$line
+
+
+    DONE!
+    The Appmonitor client was installed.
+
+    - Please edit index.php and general_include.php.
+
+    - If you have multiple applications below webroot then you can 
+      rename the file index.php to check-[appname].php eg.
+      check-cms.php, check-blog.php, ... 
+
+    - Start "$( basename "$0" )" again to perform an update.
+      Maybe you want to create a cronjob for this.
+
+INTRODUCTION
+else
+    echo "Appmonitor client was updated."
+fi
+echo
+
+echo "Documentation: $docs_url"
+echo
+echo $line
+echo done.
+cp -rp "$client_from/git_update_appmonitor.sh" "$client_to"
+
+# ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/index.php b/public_html/appmonitor/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..2587ce2f0aeb0d08ce56c1989fc83d855f4d2942
--- /dev/null
+++ b/public_html/appmonitor/index.php
@@ -0,0 +1,159 @@
+<?php
+/**
+ * IML APPMONITOR CHECKS FOR CI SERVER
+ * 
+ * @author: Axel Hahn
+ * 
+ * ------------------------------------------------------------------
+ * 2025-01-06  initial version
+ */
+
+require_once('classes/appmonitor-client.class.php');
+
+$aAppDefaults = [
+    "df" => [
+        "warning" => "1GB",
+        "critical" => "100MB"
+    ]
+];
+
+$oMonitor = new appmonitor();
+
+$sWebroot = dirname(__DIR__);
+
+include('general_include.php');
+
+$sCfgfile="$sWebroot/inc_config.php";
+$aConfig=require_once $sCfgfile;
+
+$oMonitor->addTag('ci-pkg');
+$oMonitor->addTag('rollout');
+
+if(isset($aConfig['monitor-ips']) && is_array($aConfig['monitor-ips']) && count($aConfig['monitor-ips']) > 0) {    
+    $oMonitor->checkIp($aConfig['monitor-ips']);
+}
+
+
+// ----------------------------------------------------------------------
+// config file
+// ----------------------------------------------------------------------
+
+$oMonitor->addCheck(
+    [
+        "name" => "read config file",
+        "description" => "Check if config file is readable",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $sCfgfile,
+                "file"     => true,
+                "readable" => true,
+            ],
+        ],
+    ]
+);
+
+// ----------------------------------------------------------------------
+// api and hash data
+// ----------------------------------------------------------------------
+
+$oMonitor->addCheck(
+    [
+        "name" => "check api dir",
+        "description" => "Check if api dir is readable and writable",
+        "parent" => "read config file",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $aConfig['tmpdir'],
+                "dir"     => true,
+                "readable" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+
+if (file_exists($aConfig['tmpdir'] . "/used_hashes.txt")) {
+    $oMonitor->addCheck(
+        [
+            "name" => "write hashes file",
+            "description" => "Check if hashes file is writable",
+            "parent" => "check api dir",
+            "check" => [
+                "function" => "File",
+                "params" => [
+                    "filename" => $aConfig['tmpdir'] . "/used_hashes.txt",
+                    "file"     => true,
+                    "readable" => true,
+                    "writable" => true,
+                ],
+            ],
+        ]
+    );
+}
+
+$oMonitor->addCheck(
+    [
+        "name" => "diskspace api dir",
+        "description" => "The file storage must have some space left",
+        "parent" => "check api dir",
+        "check" => [
+            "function" => "Diskfree",
+            "params" => [
+                "directory" => $aConfig['tmpdir'],
+                "warning"   => "5MB",
+                "critical"  => "100KB",
+            ],
+        ],
+    ]
+);
+
+// ----------------------------------------------------------------------
+// packages dir
+// ----------------------------------------------------------------------
+
+$oMonitor->addCheck(
+    [
+        "name" => "check package dir",
+        "description" => "Check if package dir is readable",
+        "parent" => "read config file",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $aConfig['packagedir'],
+                "dir"     => true,
+                "readable" => true,
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "diskspace package dir",
+        "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'],
+        "parent" => "check package dir",
+        "check" => [
+            "function" => "Diskfree",
+            "params" => [
+                "directory" => $aConfig['packagedir'],
+                "warning"   => $aAppDefaults["df"]['warning'],
+                "critical"  => $aAppDefaults["df"]['critical'],
+            ],
+        ],
+    ]
+);
+
+// ----------------------------------------------------------------------
+// system stuff
+// ----------------------------------------------------------------------
+
+require 'plugins/apps/shared_check_ssl.php';
+
+// ----------------------------------------------------------------------
+
+$oMonitor->setResult();
+$oMonitor->render();
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/local.php b/public_html/appmonitor/local.php
new file mode 100644
index 0000000000000000000000000000000000000000..8fcc94e1854b48c28d034745c844420c07c989c5
--- /dev/null
+++ b/public_html/appmonitor/local.php
@@ -0,0 +1,6 @@
+<?php
+
+require __DIR__ . '/check-appmonitor-server.php';
+$sJson=ob_get_contents();
+ob_end_clean();
+echo $oMonitor->renderHtmloutput($sJson);
diff --git a/public_html/appmonitor/plugins/apps/concrete5.php b/public_html/appmonitor/plugins/apps/concrete5.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa846205e0702f73170f7b9a0281071bb2578141
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/concrete5.php
@@ -0,0 +1,165 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECK
+ * ______________________________________________________________________
+ * 
+ * Check for a Concrete5 instance.
+ * CMS https://www.concrete5.org/
+ * 
+ * It checks 
+ * - the write access to the config file
+ * - the write access to the file storage
+ * - connect to mysql database (which is read from config)
+ * - ssl certificate (on https request only)
+ * 
+ * @author: Axel Hahn - https://www.axel-hahn.de/
+ * ----------------------------------------------------------------------
+ * 2018-06-30  v1.0   ah
+ * 2019-05-24  v1.01  ah                   detect include or standalone mode
+ * 2024-11-18  v1.02  <axel.hahn@unibe.ch> integrate in appmonitor repository
+ * 2024-11-22  v1.03  <axel.hahn@unibe.ch> send 400 instead of 503 on error
+ * 2024-12-21  v1.04  ah                   short array syntax; add php-modules and parent
+ * 2025-01-06  v1.05  ah                   add df
+ */
+
+// ----------------------------------------------------------------------
+// Init
+// ----------------------------------------------------------------------
+
+$aAppDefaults = [
+    "name" => "Concrete5 CMS",
+    "tags" => ["concrete5", "cms"],
+    "df" => [
+        "warning" => "100MB",
+        "critical" => "10MB"
+    ]
+];
+
+require 'inc_appcheck_start.php';
+
+// ----------------------------------------------------------------------
+// Read Concrete5 specific config items
+// ----------------------------------------------------------------------
+
+
+$sConfigfile = $sApproot . '/application/config/database.php';
+if (!file_exists($sConfigfile)) {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Config file was not found. Use ?rel=[subdir] to set the correct subdir to find /application/config/database.php.');
+}
+
+$aConfig = include($sConfigfile);
+$sActive = $aConfig['default-connection'];
+
+if (!isset($aConfig['connections'][$sActive])) {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Config file application/config/database.php was read - but database connection could not be detected from it in connections -> ' . $sActive . '.');
+}
+// print_r($aConfig['connections'][$sActive]); die();
+$aDb = $aConfig['connections'][$sActive];
+
+// ----------------------------------------------------------------------
+// checks
+// ----------------------------------------------------------------------
+
+// required php modules
+// see https://documentation.concretecms.org/developers/introduction/system-requirements
+$oMonitor->addCheck(
+    [
+        "name" => "PHP modules",
+        "description" => "Check needed PHP modules",
+        // "group" => "folder",
+        "check" => [
+            "function" => "Phpmodules",
+            "params" => [
+                "required" => [
+                    "PDO",
+                    "curl",
+                    "dom",
+                    "fileinfo",
+                    "gd",
+                    "iconv",
+                    "mbstring",
+                    "pdo_mysql",
+                    "xml",
+                    "zip"
+                ],
+                "optional" => [],
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "config file",
+        "description" => "The config file must be readable and writable",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $sConfigfile,
+                "file" => true,
+                "readable" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+$oMonitor->addCheck(
+    [
+        "name" => "check file storage",
+        "description" => "The file storage must be writable",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => "$sApproot/application/files",
+                "dir" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+
+$sPdoConnectString = "mysql:host=$aDb[server];port=3306;dbname=$aDb[database];";
+
+$oMonitor->addCheck(
+    [
+        "name" => "Mysql Master",
+        "description" => "Connect mysql server " . $aDb['server'] . " as user " . $aDb['username'] . " to scheme " . $aDb['database'],
+        "parent" => "config file",
+        "check" => [
+            "function" => "PdoConnect",
+            "params" => [
+                "connect" => $sPdoConnectString,
+                "user" => $aDb['username'],
+                "password" => $aDb['password'],
+            ],
+        ],
+    ]
+);
+
+if (isset($aAppDefaults['df'])) {
+    
+    $oMonitor->addCheck(
+        [
+            "name" => "check disk space",
+            "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'],
+            "parent" => "check file storage",
+            "check" => [
+                "function" => "Diskfree",
+                "params" => [
+                    "directory" => "$sApproot/application",
+                    "warning"   => $aAppDefaults["df"]['warning'],
+                    "critical"  => $aAppDefaults["df"]['critical'],
+                ],
+            ],
+        ]
+    );
+}
+
+// ----------------------------------------------------------------------
+
+require 'inc_appcheck_end.php';
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/apps/dokuwiki.php b/public_html/appmonitor/plugins/apps/dokuwiki.php
new file mode 100644
index 0000000000000000000000000000000000000000..c03f3a122b354f9a5f54cb8f563f94eeda27e2ed
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/dokuwiki.php
@@ -0,0 +1,150 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECK
+ * ______________________________________________________________________
+ * 
+ * Check for a Dokuwiki instance.
+ * https://www.dokuwiki.org/
+ * 
+ * @author: Axel Hahn - https://www.axel-hahn.de/
+ * ----------------------------------------------------------------------
+ * 2024-12-23  v1.00  ah                   initial version
+ * 2024-12-26  v1.01  ah                   fix directory checks
+ * 2025-01-06  v1.02  ah                   add df
+ */
+
+// ----------------------------------------------------------------------
+// Init
+// ----------------------------------------------------------------------
+
+$aAppDefaults = [
+    "name" => "Dokuwiki",
+    "tags" => ["dokuwiki", "wiki"],
+    "df" => [
+        "warning" => "100MB",
+        "critical" => "10MB"
+    ]
+];
+
+require 'inc_appcheck_start.php';
+
+// ----------------------------------------------------------------------
+// Read Concrete5 specific config items
+// ----------------------------------------------------------------------
+
+$sConfigfile = "$sApproot/conf/local.php";
+if (!file_exists($sConfigfile)) {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Config file was not found. Use ?rel=[subdir] to set the correct subdir to find /conf/local.php.');
+}
+
+// ----------------------------------------------------------------------
+// checks
+// ----------------------------------------------------------------------
+
+// required php modules
+// see https://www.dokuwiki.org/install:php
+$oMonitor->addCheck(
+    [
+        "name" => "PHP modules",
+        "description" => "Check needed PHP modules",
+        // "group" => "folder",
+        "check" => [
+            "function" => "Phpmodules",
+            "params" => [
+                "required" => [
+                    "json",
+                    "pcre",
+                    "session",
+                ],
+                "optional" => [
+                    "bz2",
+                    "gd",
+                    "intl",
+                    "mbstring",
+                    "openssl",
+                    "zlib"
+                ],
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "config file",
+        "description" => "The config file must be readable and writable",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $sConfigfile,
+                "file" => true,
+                "readable" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+
+foreach (['lib/tpl/', 'lib/plugins/',] as $sDir) {
+    $oMonitor->addCheck(
+        [
+            "name" => "check read dir $sDir",
+            "description" => "The directory $sDir must be readable",
+            "group" => "folder",
+            "check" => [
+                "function" => "File",
+                "params" => [
+                    "filename" => "$sApproot/$sDir",
+                    "dir" => true,
+                    "readable" => true,
+                ],
+            ],
+        ]
+    );
+}
+
+
+foreach (['data/attic', 'data/cache', 'data/index', 'data/locks', 'data/log', 'data/media', 'data/meta', 'data/pages', 'data/tmp',] as $sDir) {
+    $oMonitor->addCheck(
+        [
+            "name" => "check writable dir $sDir",
+            "description" => "The directory $sDir must be readable and writable",
+            "group" => "folder",
+            "check" => [
+                "function" => "File",
+                "params" => [
+                    "filename" => "$sApproot/$sDir",
+                    "dir" => true,
+                    "readable" => true,
+                    "writable" => true,
+                ],
+            ],
+        ]
+    );
+}
+
+if (isset($aAppDefaults['df'])) {
+    
+    $oMonitor->addCheck(
+        [
+            "name" => "check disk space",
+            "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'],
+            "check" => [
+                "function" => "Diskfree",
+                "params" => [
+                    "directory" => "$sApproot/data",
+                    "warning"   => $aAppDefaults["df"]['warning'],
+                    "critical"  => $aAppDefaults["df"]['critical'],
+                ],
+            ],
+        ]
+    );
+}
+
+// ----------------------------------------------------------------------
+
+require 'inc_appcheck_end.php';
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/apps/iml-appmonitor-server.php b/public_html/appmonitor/plugins/apps/iml-appmonitor-server.php
new file mode 100755
index 0000000000000000000000000000000000000000..87473021d5cbe8b47d9ac9de7b512f732e593636
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/iml-appmonitor-server.php
@@ -0,0 +1,273 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECKS for server instance
+ * ______________________________________________________________________
+ * 
+ * requires variable $sApproot
+ * 
+ * @author: Axel Hahn
+ * ----------------------------------------------------------------------
+ * 2019-04-29  aded check for ssl cert; removed a check
+ * 2019-05-17  aded check http to config- and tmp dir
+ * 2021-11-nn  removed all checks ... created as single files
+ * 2022-03-28  move checks into plugins/apps/
+ * 2024-07-23  php 8: short array syntax
+ * 2024-12-28  added check for custom config and url file (if they exist)
+ */
+
+// ----------------------------------------------------------------------
+// files and dirs
+// ----------------------------------------------------------------------
+
+$oMonitor->addCheck(
+    [
+        "name" => "PHP modules",
+        "description" => "Check needed PHP modules",
+        // "group" => "folder",
+        "check" => [
+            "function" => "Phpmodules",
+            "params" => [
+                "required" => ["curl"],
+                "optional" => [],
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "write to ./tmp/",
+        "description" => "Check cache storage",
+        // "group" => "folder",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => "$sApproot/server/tmp",
+                "dir" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+$oMonitor->addCheck(
+    [
+        "name" => "write to ./config/",
+        "description" => "Check config target directory",
+        // "group" => "folder",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => "$sApproot/server/config",
+                "dir" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "check config file",
+        "description" => "The default config file must be readable",
+        "parent" => "write to ./config/",
+        // "group" => "file",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => "$sApproot/server/config/appmonitor-server-config-defaults.json",
+                "file" => true,
+                "readable" => true,
+            ],
+        ],
+    ]
+);
+
+if (is_file("$sApproot/server/config/appmonitor-server-config.json")) {
+    $oMonitor->addCheck(
+        [
+            "name" => "check custom config file",
+            "description" => "The custom config file must be readable and writable",
+            "parent" => "write to ./config/",
+            // "group" => "file",
+            "check" => [
+                "function" => "File",
+                "params" => [
+                    "filename" => "$sApproot/server/config/appmonitor-server-config.json",
+                    "file" => true,
+                    "readable" => true,
+                    "writable" => true,
+                ],
+            ],
+        ]
+    );        
+}
+
+if (is_file("$sApproot/server/config/appmonitor-server-urls.json")) {
+    $oMonitor->addCheck(
+        [
+            "name" => "check url file",
+            "description" => "The url config file must be readable and writable",
+            "parent" => "write to ./config/",
+            // "group" => "file",
+            "check" => [
+                "function" => "File",
+                "params" => [
+                    "filename" => "$sApproot/server/config/appmonitor-server-urls.json",
+                    "file" => true,
+                    "readable" => true,
+                    "writable" => true,
+                ],
+            ],
+        ]
+    );        
+}
+
+// ----------------------------------------------------------------------
+// protect dirs against web access
+// specialty: if the test results in an error, the total result switches
+// to WARNING -> see worstresult value
+// ----------------------------------------------------------------------
+$sBaseUrl = 'http' . (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] ? 's' : '')
+    . '://' . $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT']
+    . dirname(dirname($_SERVER['REQUEST_URI']));
+
+foreach (['server/config', 'server/tmp'] as $sMyDir) {
+    $oMonitor->addCheck(
+        [
+            "name" => "http to $sMyDir",
+            "description" => "Check if the $sMyDir directory is not accessible (counts as warning on fail)",
+            "group" => "deny",
+            "check" => [
+                "function" => "HttpContent",
+                "params" => [
+                    "url" => "$sBaseUrl/$sMyDir/readme.md",
+                    "status" => 403,
+                ],
+            ],
+            "worstresult" => RESULT_WARNING
+        ]
+    );
+}
+
+// ----------------------------------------------------------------------
+// count of current projects
+// ----------------------------------------------------------------------
+require_once($sApproot . '/server/classes/appmonitor-server.class.php');
+$oServer = new appmonitorserver();
+$iCount = count($oServer->getAppIds());
+$oMonitor->addCheck(
+    [
+        "name" => "appcounter",
+        "description" => "Monitored apps",
+        "group" => "monitor",
+        "parent" => false,
+        "check" => [
+            "function" => "Simple",
+            "params" => [
+                "result" => RESULT_OK,
+                "value" => "Found monitored web apps: $iCount",
+                "count" => $iCount,
+                "visual" => "simple",
+            ],
+        ],
+    ]
+);
+// ----------------------------------------------------------------------
+// check running service
+// ----------------------------------------------------------------------
+require_once($sApproot . '/server/classes/tinyservice.class.php');
+ob_start();
+$oService = new tinyservice("$sApproot/server/service.php", 15, "$sApproot/server/tmp");
+$sIsStopped = $oService->canStart();
+$out = ob_get_contents();
+ob_clean();
+$oMonitor->addCheck(
+    [
+        "name" => "running service",
+        "description" => "Check if the service is running",
+        "group" => "service",
+        "check" => [
+            "function" => "Simple",
+            "params" => [
+                "result" => ($sIsStopped ? RESULT_WARNING : RESULT_OK),
+                "value" => ($sIsStopped
+                    ? "Info: Service is NOT running. Apps are checked interactively only (if the appmonitor web ui is running). | Output: $out"
+                    : "OK, service is running. | Output: $out"
+                )
+            ],
+        ],
+        "worstresult" => RESULT_OK
+    ]
+);
+// ----------------------------------------------------------------------
+// check certificate if https is used
+// ----------------------------------------------------------------------
+include 'shared_check_ssl.php';
+
+
+$oMonitor->addCheck(
+    [
+        "name" => "plugin Load",
+        "description" => "current load",
+        "group" => 'monitor',
+        "parent" => false,
+        "check" => [
+            "function" => "Loadmeter",
+            "params" => [
+                "warning" => 1.0,
+                "error" => 3,
+            ],
+        ],
+        "worstresult" => RESULT_OK
+    ]
+);
+
+// ----------------------------------------------------------------------
+// plugin test
+// ----------------------------------------------------------------------
+/*
+ * 
+ * AS A DEMO: using a custom plugin:
+ * 
+$oMonitor->addCheck(
+    [
+        "name" => "plugin test",
+        "description" => "minimal test of the plugin plugins/checkHello.php",
+        "check" => [
+            "function" => "Hello",
+            "params" => []
+                "message" => "Here I am",
+            ],
+        ],
+    ]
+);
+$oMonitor->addCheck(
+    [
+        "name" => "plugin Load",
+        "description" => "check current load",
+        "check" => [
+            "function" => "Loadmeter",
+            "params" => [
+                "warning" => 1.0,
+                "error" => 3,
+            ],
+        ],
+        "worstresult" => RESULT_OK
+    ]
+);
+$oMonitor->addCheck(
+    [
+        "name" => "plugin ApacheProcesses",
+        "description" => "check count running Apache processes",
+        "check" => [
+            "function" => "ApacheProcesses",
+            "params" => [
+            ],
+        ],
+        "worstresult" => RESULT_OK
+    ]
+);
+*/
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/apps/inc_appcheck_end.php b/public_html/appmonitor/plugins/apps/inc_appcheck_end.php
new file mode 100644
index 0000000000000000000000000000000000000000..e939ed6d5c31046af8b140082395c6aec270ad85
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/inc_appcheck_end.php
@@ -0,0 +1,35 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - INCLUDE FOR APP CHECKS :: ON END
+ * ______________________________________________________________________
+ */
+
+include 'shared_check_ssl.php';
+
+// $bStandalone was set in inc_appcheck_start.php
+// send response if client was not initialized there
+if ($bStandalone) {
+
+    if(count($oMonitor->getChecks())==0){
+
+        $oMonitor->addCheck(
+            [
+                "name" => "Simple",
+                "description" => "Welcome to a simple app check. This is just a quick winner.",
+                "check" => [
+                    "function" => "Simple",
+                    "params" => [
+                        "result" => RESULT_OK,
+                        "value" => "Create a custom check and add all checks you need to test the ability to run the application",
+                    ],
+                ],
+            ]
+        );
+    }
+
+    $oMonitor->setResult();
+    $oMonitor->render();
+}
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/apps/inc_appcheck_start.php b/public_html/appmonitor/plugins/apps/inc_appcheck_start.php
new file mode 100644
index 0000000000000000000000000000000000000000..3218fc99bd70bff01a289fbf65af2b793cdf1a45
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/inc_appcheck_start.php
@@ -0,0 +1,62 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - INCLUDE FOR APP CHECKS :: ON START
+ * ______________________________________________________________________
+ */
+
+// ----------------------------------------------------------------------
+// CHECK IF THE APPROOT IS SET
+// ----------------------------------------------------------------------
+
+// initialize client and set very basic metadata ... if needed
+$bStandalone = !(class_exists('appmonitor') && isset($oMonitor));
+if ($bStandalone) {
+    require_once __DIR__ . '/../../classes/appmonitor-client.class.php';
+    $oMonitor = new appmonitor();
+
+    if (!isset($sApproot) || empty($sApproot)) {
+        $sApproot = $_SERVER['DOCUMENT_ROOT'];
+        if (isset($_GET['rel'])) {
+            $sApproot .= str_replace('..', '__', $_GET['rel']);
+            if (!is_dir($sApproot)) {
+                header('HTTP/1.0 400 Bad request');
+                die('ERROR: The given rel dir does not exist below webroot.');
+            }
+        }
+    }
+
+    // --- set values coming from app plugins defaults & GET params 
+    // "name"
+    // "host"
+    // "tags"    
+    // "dfw", "dfc"    
+    $aAppDefaults['name'] = (isset($_GET['name']) && $_GET['name']) ? $_GET['name'] : $aAppDefaults['name'];
+    $aAppDefaults['host'] = $_GET['host']
+        ? explode(',', $_GET['host'])
+        : ($_SERVER['HTTP_HOST'] ?? '');
+    $aAppDefaults['tags'] = $_GET['tags'] ? explode(',', $_GET['tags']) : $aAppDefaults['tags'];
+
+    $aAppDefaults['df']['warning'] = (isset($_GET['dfw']) && $_GET['dfw']) ? $_GET['dfw'] : $aAppDefaults['df']['warning'] ?? false;
+    $aAppDefaults['df']['critical'] = (isset($_GET['dfc']) && $_GET['dfc']) ? $_GET['dfc'] : $aAppDefaults['df']['critical'] ?? false;
+    if($aAppDefaults['df']['warning'] == false && $aAppDefaults['df']['critical'] == false) {
+        unset($aAppDefaults['df']);
+    }
+
+    if ($aAppDefaults['name']) {
+        $oMonitor->setWebsite($aAppDefaults['name']);
+    }
+    if ($aAppDefaults['host']) {
+        $oMonitor->setHost($aAppDefaults['host']);
+    }
+    ;
+    if (isset($aAppDefaults['tags']) && is_array($aAppDefaults['tags']) && count($aAppDefaults['tags']) > 0) {
+        foreach ($aAppDefaults['tags'] as $sTag) {
+            $oMonitor->addTag($sTag);
+        }
+    }
+
+    @include __DIR__ . '/../../general_include.php';
+}
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/apps/matomo.php b/public_html/appmonitor/plugins/apps/matomo.php
new file mode 100644
index 0000000000000000000000000000000000000000..3be9f6e55adf6b9da63800a8ced5977ec5f98c69
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/matomo.php
@@ -0,0 +1,156 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECK
+ * ______________________________________________________________________
+ * 
+ * Check for a Matomo instance.
+ * Open Analytics platform - https://matomo.org/
+ * 
+ * It checks 
+ * - the write access to the config file
+ * - connect to matomo database (which is read from config)
+ * - ssl certificate (on https request only)
+ * 
+ * @author: Axel Hahn - https://www.axel-hahn.de/
+ * ----------------------------------------------------------------------
+ * 2018-06-30  v1.0
+ * 2019-05-24  v1.01  detect include or standalone mode
+ * 2019-05-24  v1.02  detect include or standalone mode
+ * 2024-12-20  v1.03  <axel.hahn@unibe.ch> integrate in appmonitor repository
+ * 2024-12-21  v1.04  ah                   add php-modules and parent
+ * 2025-01-06  v1.05  ah                   add checks for writable dirs; add df
+ */
+
+// ----------------------------------------------------------------------
+// Init
+// ----------------------------------------------------------------------
+
+$aAppDefaults = [
+    "name" => "Matomo web statistics",
+    "tags" => ["matomo", "statistics"],
+    "df" => [
+        "warning" => "100MB",
+        "critical" => "10MB"
+    ]
+];
+
+require 'inc_appcheck_start.php';
+
+// ----------------------------------------------------------------------
+// Read Matomo specific config items
+// ----------------------------------------------------------------------
+
+$sConfigfile = $sApproot . '/config/config.ini.php';
+if (!file_exists($sConfigfile)) {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Config file was not found. Set a correct $sApproot pointing to Matomo install dir.');
+}
+$aConfig = parse_ini_file($sConfigfile, true);
+
+
+// ----------------------------------------------------------------------
+// checks
+// ----------------------------------------------------------------------
+
+// required php modules
+// see https://matomo.org/faq/on-premise/matomo-requirements/
+$oMonitor->addCheck(
+    [
+        "name" => "PHP modules",
+        "description" => "Check needed PHP modules",
+        // "group" => "folder",
+        "check" => [
+            "function" => "Phpmodules",
+            "params" => [
+                "required" => [
+                    "PDO",
+                    "curl",
+                    "gd",
+                    "mbstring",
+                    "pdo_mysql",
+                    "xml",
+                ],
+                "optional" => [],
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "config file",
+        "description" => "The config file must be readable and writable",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $sConfigfile,
+                "file" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "Mysql Connect",
+        "description" => "Connect mysql server " . $aConfig['database']['host'] . " as user " . $aConfig['database']['username'] . " to scheme " . $aConfig['database']['dbname'],
+        "parent" => "config file",
+        "check" => [
+            "function" => "MysqlConnect",
+            "params" => [
+                "server" => $aConfig['database']['host'],
+                "user" => $aConfig['database']['username'],
+                "password" => $aConfig['database']['password'],
+                "db" => $aConfig['database']['dbname'],
+            ],
+        ],
+    ]
+);
+
+
+// directory list from system check
+foreach (['/tmp', '/tmp/assets', '/tmp/cache', '/tmp/climulti', '/tmp/latest', '/tmp/logs', '/tmp/sessions', '/tmp/tcpdf', '/tmp/templates_c'] as $sDir) {
+    $oMonitor->addCheck(
+        [
+            "name" => "check writable dir $sDir",
+            "description" => "The directory $sDir must be readable and writable",
+            "group" => "folder",
+            "check" => [
+                "function" => "File",
+                "params" => [
+                    "filename" => "$sApproot/$sDir",
+                    "dir" => true,
+                    "readable" => true,
+                    "writable" => true,
+                ],
+            ],
+        ]
+    );
+}
+
+
+if (isset($aAppDefaults['df'])) {
+    
+    $oMonitor->addCheck(
+        [
+            "name" => "check disk space",
+            "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'],
+            "check" => [
+                "function" => "Diskfree",
+                "params" => [
+                    "directory" => $sApproot,
+                    "warning"   => $aAppDefaults["df"]['warning'],
+                    "critical"  => $aAppDefaults["df"]['critical'],
+                ],
+            ],
+        ]
+    );
+}
+
+// ----------------------------------------------------------------------
+
+require 'inc_appcheck_end.php';
+
+// ----------------------------------------------------------------------
\ No newline at end of file
diff --git a/public_html/appmonitor/plugins/apps/nextcloud.php b/public_html/appmonitor/plugins/apps/nextcloud.php
new file mode 100644
index 0000000000000000000000000000000000000000..231fbb0ad95a459a0001c96c8d730e8e6b79bc99
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/nextcloud.php
@@ -0,0 +1,235 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECK
+ * ______________________________________________________________________
+ * 
+ * Check for a Nextcloud instance.
+ * 
+ * It checks 
+ * - the read + write access to the config file
+ * - connect to database (which is read from config)
+ * - the read + write access to data dir
+ * - free disk space on data dir
+ * - ssl certificate (on https request only)
+ * 
+ * @author: Axel Hahn - https://www.axel-hahn.de/
+ * ----------------------------------------------------------------------
+ * 2025-01-02  v1.0
+ */
+
+// ----------------------------------------------------------------------
+// Init
+// ----------------------------------------------------------------------
+
+$aAppDefaults = [
+    "name" => "Nextcloud",
+    "tags" => ["nextcloud", "share"],
+    "df" => [
+        "warning" => "1GB",
+        "critical" => "100MB"
+    ]
+];
+
+require 'inc_appcheck_start.php';
+
+// ----------------------------------------------------------------------
+// Read Nextcloud specific config items
+// ----------------------------------------------------------------------
+
+$sConfigfile = "$sApproot/config/config.php";
+if (!file_exists($sConfigfile)) {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Config file was not found. Use ?rel=/NAME or similiar to set a relative install dir.');
+}
+
+if (!include "$sConfigfile") {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Unable to read config file.');
+}
+
+// now $CONFIG is available ...
+/*
+Array
+(
+    [instanceid] => ocw...
+    [passwordsalt] => cNs...
+    [secret] => kFdQXw2w...
+    [trusted_domains] => Array
+        (
+            [0] => https://www.example.com
+        )
+
+    [datadirectory] => /home/httpd/cloud/data
+    [dbtype] => mysql
+    [version] => 30.0.4.1
+    [overwrite.cli.url] => https://www.example.com/cloud
+    [dbname] => nextcloud
+    [dbhost] => 127.0.0.1
+    [dbport] => 
+    [dbtableprefix] => oc_
+    [mysql.utf8mb4] => 1
+    [dbuser] => mydbuser
+    [dbpassword] => 516px9kcc...
+    [installed] => 1
+    [maintenance] => 
+    [theme] => 
+    [loglevel] => 2
+    [mail_smtpmode] => smtp
+    [mail_sendmailmode] => smtp
+)
+*/
+
+if (!isset($CONFIG) || !is_array($CONFIG)) {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Config file was found but has unexpected format.');
+} 
+
+
+// ----------------------------------------------------------------------
+// checks
+// ----------------------------------------------------------------------
+
+// required php modules
+// see https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html
+// doesn't show needed modules
+/*
+$oMonitor->addCheck(
+    [
+        "name" => "PHP modules",
+        "description" => "Check needed PHP modules",
+        // "group" => "folder",
+        "check" => [
+            "function" => "Phpmodules",
+            "params" => [
+                "required" => [
+                    "PDO",
+                    "curl",
+                    "gd",
+                    "mbstring",
+                    "pdo_mysql",
+                    "xml",
+                ],
+                "optional" => [],
+            ],
+        ],
+    ]
+);
+*/
+
+$oMonitor->addCheck(
+    [
+        "name" => "config file",
+        "description" => "The config file must be readable and writable",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $sConfigfile,
+                "file" => true,
+                "readable" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+$oMonitor->addCheck(
+    [
+        "name" => "Version",
+        "description" => "Nextcloud version",
+        "parent" => "config file",
+        "check" => [
+            "function" => "Simple",
+            "params" => [
+                "result" => RESULT_OK,
+                "value" => $CONFIG['version'] ?? "??",
+                "count" => $CONFIG['version'] ?? "??",
+                "visual" => "simple"
+            ],
+        ],
+    ]
+);
+$oMonitor->addCheck(
+    [
+        "name" => "maintenance",
+        "description" => "Is maintenance mode enabled?",
+        "parent" => "config file",
+        "check" => [
+            "function" => "Simple",
+            "params" => [
+                "result" => $CONFIG['maintenance'] ? RESULT_ERROR : RESULT_OK,
+                "value" => $CONFIG['maintenance'] ? "enabled - public access is denied" : "disabled (OK)",
+            ],
+        ],
+    ]
+);
+
+// ----------------------------------------------------------------------
+// database
+// ----------------------------------------------------------------------
+
+if($CONFIG['dbtype'] == "mysql"){
+    $oMonitor->addCheck(
+        [
+            "name" => "Mysql Connect",
+            // "description" => "Connect mysql server " . $aConfig['database']['host'] . " as user " . $aConfig['database']['username'] . " to scheme " . $aConfig['database']['dbname'],
+            "description" => "Connect mysql server",
+            "parent" => "config file",
+            "check" => [
+                "function" => "MysqlConnect",
+                "params" => [
+                    "server" => $CONFIG['dbhost'],
+                    "user" => $CONFIG['dbuser'],
+                    "password" => $CONFIG['dbpassword'],
+                    "db" => $CONFIG['dbname'],
+                    "port" => $CONFIG['dbport'],
+                ],
+            ],
+        ]
+    );
+}
+
+// ----------------------------------------------------------------------
+// data directory
+// ----------------------------------------------------------------------
+
+$oMonitor->addCheck(
+    [
+        "name" => "data dir",
+        "description" => "Data directory must be readable and writable",
+        "parent" => "config file",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $CONFIG['datadirectory'],
+                "dir" => true,
+                "readable" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+
+if (isset($aAppDefaults['df'])) {
+    
+    $oMonitor->addCheck(
+        [
+            "name" => "check disk space",
+            "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'],
+            "parent" => "data dir",
+            "check" => [
+                "function" => "Diskfree",
+                "params" => [
+                    "directory" => $CONFIG['datadirectory'],
+                    "warning"   => $aAppDefaults["df"]['warning'],
+                    "critical"  => $aAppDefaults["df"]['critical'],
+                ],
+            ],
+        ]
+    );
+}
+
+// ----------------------------------------------------------------------
+
+require 'inc_appcheck_end.php';
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/apps/shared_check_ssl.php b/public_html/appmonitor/plugins/apps/shared_check_ssl.php
new file mode 100644
index 0000000000000000000000000000000000000000..c80a3b8da347af8e1d5eefe20dc2d6017c30e91a
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/shared_check_ssl.php
@@ -0,0 +1,33 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * A P P M O N I T O R  ::  GENERIC CHECK SSL CERT
+ * 
+ * If https is enabled on standard port 443 the validity of the
+ * certificate.
+ * If the current appplication uses http only this check does nothing.
+ * 
+ * ______________________________________________________________________
+ * 
+ * @author: Axel Hahn
+ * ----------------------------------------------------------------------
+ * 2022-03-28  created
+ */
+
+
+// ----------------------------------------------------------------------
+// check certificate - only if https is used
+// ----------------------------------------------------------------------
+if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']) {
+    $oMonitor->addCheck(
+        [
+            "name" => "Certificate check",
+            "description" => "Check if SSL cert is valid and does not expire soon",
+            "check" => [
+                "function" => "Cert",
+            ],
+        ]
+    );
+}
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/apps/wordpress.php b/public_html/appmonitor/plugins/apps/wordpress.php
new file mode 100644
index 0000000000000000000000000000000000000000..1ac052034d32a09d01b14fc6dae2e730f79c2160
--- /dev/null
+++ b/public_html/appmonitor/plugins/apps/wordpress.php
@@ -0,0 +1,160 @@
+<?php
+/* ______________________________________________________________________
+ * 
+ * WORK IN PROGRESS
+ * 
+ * A P P M O N I T O R  ::  CLIENT - CHECK
+ * ______________________________________________________________________
+ * 
+ * Check for a Wordpress instance.
+ * Blogsoftware https://wordpress.org/
+ * 
+ * It checks 
+ * - the write access to the config file
+ * - connect to mysql database (which is read from config)
+ * - ssl certificate (on https request only)
+ * 
+ * @author: <axel.hahn@unibe.ch>
+ * ----------------------------------------------------------------------
+ * 2018-11-07  v0.01
+ * 2019-05-24  v0.02  detect include or standalone mode
+ * 2019-05-24  v0.03  detect include or standalone mode
+ * 2024-07-31  v0.04  first version for wordpress check in plugins/apps/ 
+ * 2024-11-21  v0.05  use shared_check_sl 
+ * 2024-11-22  v0.07  <axel.hahn@unibe.ch> send 400 instead of 503 on error
+ * 2024-12-21  v0.08  ah                   add php-modules and parent
+ * 2025-01-06  v1.05  ah                   add df
+ */
+
+// ----------------------------------------------------------------------
+// Init
+// ----------------------------------------------------------------------
+
+$aAppDefaults = [
+    "name" => "Wordpress",
+    "tags" => ["wordpress", "blog"],
+    "df" => [
+        "warning" => "100MB",
+        "critical" => "10MB"
+    ]
+];
+
+require 'inc_appcheck_start.php';
+
+// ----------------------------------------------------------------------
+// Read config items
+// ----------------------------------------------------------------------
+
+$sConfigfile = $sApproot . '/wp-config.php';
+if (!file_exists($sConfigfile)) {
+    header('HTTP/1.0 400 Bad request');
+    die('ERROR: Config file [wp-config.php] was not found. Set a correct app root pointing to wordpress install dir.');
+}
+
+require($sConfigfile);
+$aDb = [
+    'server' => DB_HOST,
+    'username' => DB_USER,
+    'password' => DB_PASSWORD,
+    'database' => DB_NAME,
+    // 'port'     => ??,
+];
+
+// ----------------------------------------------------------------------
+// checks
+// ----------------------------------------------------------------------
+
+// required php modules
+// see https://ertano.com/required-php-modules-for-wordpress/
+$oMonitor->addCheck(
+    [
+        "name" => "PHP modules",
+        "description" => "Check needed PHP modules",
+        // "group" => "folder",
+        "check" => [
+            "function" => "Phpmodules",
+            "params" => [
+                "required" => [
+                    // "cmath",
+                    "cli",
+                    "curl",
+                    "date",
+                    "dom",
+                    "fileinfo",
+                    "filter",
+                    "gd",
+                    "gettext",
+                    "hash",
+                    "iconv",
+                    "imagick",
+                    "json",
+                    // "libsodium",
+                    "mysql",
+                    "openssl",
+                    "pcre",
+                    // "opcache",
+                    // "readline",
+                    "xml",
+                    "zip"
+                ],
+                "optional" => [],
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "config file",
+        "description" => "The config file must be writable",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $sConfigfile,
+                "file" => true,
+                "readable" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+
+$oMonitor->addCheck(
+    [
+        "name" => "Mysql Connect",
+        "description" => "Connect mysql server " . $aDb['server'] . " as user " . $aDb['username'] . " to scheme " . $aDb['database'],
+        "parent" => "config file",
+        "check" => [
+            "function" => "MysqlConnect",
+            "params" => [
+                "server" => $aDb['server'],
+                "user" => $aDb['username'],
+                "password" => $aDb['password'],
+                "db" => $aDb['database'],
+                // "port"     => $aDb['port'],
+            ],
+        ],
+    ]
+);
+
+if (isset($aAppDefaults['df'])) {
+    
+    $oMonitor->addCheck(
+        [
+            "name" => "check disk space",
+            "description" => "The file storage must have some space left - warn: " . $aAppDefaults["df"]['warning'] . "/ critical: " . $aAppDefaults["df"]['critical'],
+            "check" => [
+                "function" => "Diskfree",
+                "params" => [
+                    "directory" => "$sApproot",
+                    "warning"   => $aAppDefaults["df"]['warning'],
+                    "critical"  => $aAppDefaults["df"]['critical'],
+                ],
+            ],
+        ]
+    );
+}
+
+require 'inc_appcheck_end.php';
+
+// ----------------------------------------------------------------------
diff --git a/public_html/appmonitor/plugins/checks/apacheprocesses.php b/public_html/appmonitor/plugins/checks/apacheprocesses.php
new file mode 100755
index 0000000000000000000000000000000000000000..400c880bf9d2724e6adda5caa3df45d156aa12e7
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/apacheprocesses.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * SHOW COUNT OF ACTIVE APACHE PROCESSES
+ * ____________________________________________________________________________
+ * 
+ * PARAMS:
+ *   url     {string}   optional: override https server-status page; default is http://localhost/server-status
+ *   warning {integer}  optional: limit to switch to warning (in percent); default: 50
+ *   error   {integer}  optional: limit to switch to error (in percent); default: 75
+ * 
+ * USAGE:
+ * Example with overriding all existing params
+ * 
+ * $oMonitor->addCheck(
+ *    [
+ *         "name" => "plugin ApacheProcesses",
+ *         "description" => "check count running Apache processes",
+ *         "check" => [
+ *             "function" => "ApacheProcesses",
+ *             "params" => [
+ *                 "url" => "https://localhost/status",
+ *                 "warning" => 75,
+ *                 "error" => 90,
+ *             ],
+ *         ],
+ *         "worstresult" => RESULT_OK
+ *     ]
+ * );
+ * ____________________________________________________________________________
+ * 
+ * 2019-06-07  <axel.hahn@iml.unibe.ch>
+ * 2022-07-06  <axel.hahn@iml.unibe.ch>  set group "monitor"
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkApacheProcesses extends appmonitorcheck
+{
+
+    /**
+     * url of server status
+     * @var string
+     */
+    protected string $_sServerStatusUrl = 'http://localhost/server-status';
+
+    /**
+     * Warning level in percent
+     * @var float
+     */
+    protected float $_iWarn = 50;
+
+    /**
+     * Critical level in percent
+     * @var float
+     */
+    protected float $_iError = 75;
+
+    /**
+     * Self documentation (as idea)
+     * @return array
+     */
+    public function explain(): array
+    {
+        return [
+            'name' => 'Plugin ApacheProcesses',
+            'descriptionm' => 'Check count running Apache processes',
+            'parameters' => [
+                'url' => [
+                    'type' => 'string',
+                    'required' => false,
+                    'decsription' => 'Override https server-status page; default is http://localhost/server-status; Use it if the protocol to localhost is not http, but https or if it requires an authentication',
+                    'default' => $this->_sServerStatusUrl,
+                    'example' => '',
+                ],
+                'warning' => [
+                    'type' => 'float',
+                    'required' => false,
+                    'decsription' => 'Limit to switch to warning (in percent)',
+                    'default' => $this->_iWarn,
+                    'example' => 30,
+                ],
+                'error' => [
+                    'type' => 'float',
+                    'required' => false,
+                    'decsription' => 'Limit to switch to critical (in percent)',
+                    'default' => $this->_iError,
+                    'example' => 50,
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * Fetch http server status and return slots, active and waiting processes
+     * as array i.e. [total] => 256 \n    [free] => 247\n    [waiting] => 7\n    [active] => 2
+     * It returns false if the url is not reachable
+     * It returns an empty array if the server status could not be parsed from http response
+     * @return array
+     */
+    protected function _getApacheProcesses(): bool|array
+    {
+        $sBody = file_get_contents($this->_sServerStatusUrl);
+        if (!$sBody) {
+            return false;
+        }
+        $sRegexScoreboard = '/<pre>(.*)\<\/pre\>/U';
+        $aScore = [];
+        $sStatusNobr = str_replace("\n", "", $sBody);
+
+        if (preg_match_all($sRegexScoreboard, $sStatusNobr, $aTmpTable)) {
+            $sScoreString = $aTmpTable[1][0];
+            // $aScore['scoreboard']=$sScoreString;
+            $aScore['total'] = strlen($sScoreString);
+            $aScore['free'] = substr_count($sScoreString, '.');
+            $aScore['waiting'] = substr_count($sScoreString, '_');
+            $aScore['active'] = $aScore['total'] - $aScore['free'] - $aScore['waiting'];
+        }
+        return $aScore;
+    }
+
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'monitor';
+    }
+
+    /**
+     * Implemented method: run the check
+     * @param array $aParams  parameters
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+
+        // --- (1) verify if array key(s) exist:
+        // $this->_checkArrayKeys($aParams, "...");
+        if (isset($aParams['url']) && $aParams['url']) {
+            $this->_sServerStatusUrl = $aParams['url'];
+        }
+        if (isset($aParams['warning']) && (int) $aParams['warning']) {
+            $this->_iWarn = (int) $aParams['warning'];
+        }
+        if (isset($aParams['error']) && (int) $aParams['error']) {
+            $this->_iError = (int) $aParams['error'];
+        }
+
+        // --- (2) do something magic
+        $aProcesses = $this->_getApacheProcesses();
+        $iActive = $aProcesses ? $aProcesses['active'] : false;
+
+        // set result code
+        if ($iActive === false) {
+            $iResult = RESULT_UNKNOWN;
+        } else {
+            $sComment = '';
+            $iTotal = $aProcesses['total'];
+            $iResult = RESULT_OK;
+            if (($iActive / $iTotal * 100) > $this->_iWarn) {
+                $iResult = RESULT_WARNING;
+                $sComment = "more than warning level $this->_iWarn %";
+            } else {
+                $sComment = "less than warning level $this->_iWarn %";
+            }
+            if (($iActive / $iTotal * 100) > $this->_iError) {
+                $iResult = RESULT_ERROR;
+                $sComment = "more than error level $this->_iError %";
+            }
+        }
+
+        // --- (3) response
+        // see method appmonitorcheck->_setReturn()
+        // 
+        // {integer} you should use a RESULT_XYZ constant:
+        //              RESULT_OK|RESULT_UNKNOWN|RESULT_WARNING|RESULT_ERROR
+        // {string}  output text 
+        // {array}   optional: counter data
+        //              type   => {string} "counter"
+        //              count  => {float}  value
+        //              visual => {string} one of bar|line|simple (+params)
+        //           
+        return [
+            $iResult,
+            ($iActive === false ? 'Apache httpd server status is not available' : 'apache processes: ' . print_r($aProcesses, 1)) . ' ' . $sComment,
+            ($iActive === false
+                ? []
+                : [
+                    'type' => 'counter',
+                    'count' => $iActive,
+                    'visual' => 'line',
+                ]
+            )
+        ];
+    }
+}
diff --git a/public_html/appmonitor/plugins/checks/cert.php b/public_html/appmonitor/plugins/checks/cert.php
new file mode 100755
index 0000000000000000000000000000000000000000..a6705b23df2429cbeca41d00d63add35432eb81a
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/cert.php
@@ -0,0 +1,122 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CUSTOM CHECK
+ * 
+ * Check expiration of ssl certificate
+ * ____________________________________________________________________________
+ * 
+ * USAGE:
+ * 
+ * $oMonitor->addCheck(
+ *     [
+ *         "name" => "SSL cert",
+ *         "description" => "Check SSL certificate of my domain",
+ *         "check" => [
+ *             "function" => "Cert",
+ *             "params" => [
+ *                 "url" => "https://www.example.com",
+ *                 "warning" => "30",
+ *             ],
+ *         ],
+ *     ]
+ * );
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-26  <axel.hahn@iml.unibe.ch>
+ * 2022-05-02  <axel.hahn@iml.unibe.ch>  set warning to 21 days (old value was 30); add "critical" param
+ * 2022-05-03  <axel.hahn@iml.unibe.ch>  critical limit is a warning only (because app is still functional)
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkCert extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'security';
+    }
+
+    /**
+     * Check SSL certificate 
+     * @param array $aParams
+     * [
+     *     "url"       optional: url to connect check; default: own protocol + server
+     *     "verify"    optional: flag for verification of certificate or check for expiration only; default=true (=verification is on)
+     *     "warning"   optional: count of days to warn; default=21 (=3 weeks)
+     *     "critical"  optional: count of days to raise critical; default=5
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $sUrl = $aParams["url"] ?? 'http' . ($_SERVER['HTTPS'] ? 's' : '') . '://' . $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT'];
+        $bVerify = isset($aParams["verify"]) ? !!$aParams["verify"] : true;
+        $iWarn = isset($aParams["warning"]) ? (int) ($aParams["warning"]) : 21;
+        $iCrtitcal = isset($aParams["critical"]) ? (int) ($aParams["critical"]) : 5;
+
+        $sMessage = "Checked url: $sUrl ... ";
+        $certinfo = $this->_certGetInfos($sUrl, $bVerify);
+        if (isset($certinfo['_error'])) {
+            return [
+                RESULT_ERROR,
+                $certinfo['_error'] . $sMessage
+            ];
+        }
+
+        $sDNS = $certinfo['extensions']['subjectAltName'] ?? false;
+        $sHost = parse_url($sUrl, PHP_URL_HOST);
+        if (strstr($sDNS, "DNS:$sHost") === false) {
+            return [
+                RESULT_ERROR,
+                "Wrong certificate: $sHost is not listed as DNS alias in [$sDNS]. $sMessage"
+            ];
+        }
+
+        $iDaysleft = round(($certinfo['validTo_time_t'] - date('U')) / 60 / 60 / 24);
+        $sMessage .= 'Issuer: ' . $certinfo['issuer']['O']
+            . '; valid from: ' . date("Y-m-d H:i", $certinfo['validFrom_time_t'])
+            . ' to ' . date("Y-m-d H:i", $certinfo['validTo_time_t']) . ' '
+            . ($iDaysleft ? "($iDaysleft days left)" : "expired since " . (-$iDaysleft) . " days.")
+        ;
+        if ($iDaysleft <= 0) {
+            return [
+                RESULT_ERROR,
+                'Expired! ' . $sMessage
+            ];
+        }
+        if ($iDaysleft <= $iWarn) {
+            return [
+                RESULT_WARNING,
+                ($iDaysleft <= $iCrtitcal
+                    ? 'Expires very soon! '
+                    : 'Expires soon. '
+                ) . $sMessage
+            ];
+        }
+        // echo '<pre>';
+        return [
+            RESULT_OK,
+            'OK. '
+            . ($bVerify ? 'Certificate is valid. ' : '(Verification is disabled; Check for expiration only.) ')
+            . $sMessage
+        ];
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/diskfree.php b/public_html/appmonitor/plugins/checks/diskfree.php
new file mode 100755
index 0000000000000000000000000000000000000000..86e8efc8d4f74547b9e1647074167e59cb3af206
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/diskfree.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK FOR FREE DISKSPACE
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-26  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 2025-01-02  <www.axel-hahn.de>        update output
+ */
+class checkDiskfree extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'disk';
+    }
+
+    /**
+     * Check free disk space on a given directory
+     * @param array $aParams
+     * [
+     *     "directory"   directory that must exist
+     *     "warning"     space for warning (optional)
+     *     "critical"    minimal space
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $this->_checkArrayKeys($aParams, "directory,critical");
+
+        $sDirectory = $aParams["directory"];
+        if (!is_dir($sDirectory)) {
+            return [
+                RESULT_ERROR,
+                "directory [$sDirectory] does not exist. Maybe it is wrong or is not mounted."
+            ];
+        }
+
+        $iWarn = isset($aParams["warning"]) ? $this->_getSize($aParams["warning"]) : false;
+        $iCritical = $this->_getSize($aParams["critical"]);
+        $iSpaceLeft = disk_free_space($sDirectory);
+
+        $sMessage = $this->_getHrSize($iSpaceLeft) . ' left in [' . $sDirectory . '].';
+
+        if ($iWarn) {
+            if ($iWarn <= $iCritical) {
+                header('HTTP/1.0 503 Service Unavailable');
+                die("ERROR in a Diskfree check - warning value must be larger than critical.<pre>" . print_r($aParams, true));
+            }
+            if ($iWarn < $iSpaceLeft) {
+                return [
+                    RESULT_OK,
+                    "$sMessage Warning level is not reached yet (still " . $this->_getHrSize($iSpaceLeft - $iWarn) . " over warning limit)."
+                ];
+            }
+            if ($iWarn > $iSpaceLeft && $iCritical < $iSpaceLeft) {
+                return [
+                    RESULT_WARNING,
+                    $sMessage . ' Warning level ' . $this->_getHrSize($iWarn) . ' was reached (space is ' . $this->_getHrSize($iWarn - $iSpaceLeft) . ' below warning limit; still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).'
+                ];
+            }
+        }
+        // check space
+        if ($iCritical < $iSpaceLeft) {
+            return [RESULT_OK, $sMessage . ' Minimum is not reached yet (still ' . $this->_getHrSize($iSpaceLeft - $iCritical) . ' over critical limit).'];
+        } else {
+            return [RESULT_ERROR, $sMessage];
+        }
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/exec.php b/public_html/appmonitor/plugins/checks/exec.php
new file mode 100644
index 0000000000000000000000000000000000000000..6221ca6fed432c724e4339e6b030d7cc73fd3e14
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/exec.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CUSTOM CHECK BASED ON SHELL COMMANDS
+ * 
+ * Execute a shell command.
+ * ____________________________________________________________________________
+ * 
+ * 2022-09-19  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkExec extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup()
+    {
+        return 'service';
+    }
+
+    /**
+     * Check execution of a command
+     * @param array $aParams
+     * [
+     *     "command"        {string} command to execute
+     *     "output"         {bool}   flag: show output; default: true
+     *
+     *     "exitOK"         {array}  array of integegers for ok exitcodes
+     *     "exitWarn"       {array}  array of integegers for exitcodes with warning
+     *     "exitCritical"   {array}  array of integegers for exitcodes that result in an error
+     *
+     *     // TODO ... MAYBE
+     *     "searchOK"       {string} search string that must be found in output
+     *     "searchWarn"     {string} if search string is found check returns with warning
+     *     "searchCritical" {string} if search string is found check returns with critical
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $this->_checkArrayKeys($aParams, "command");
+        $_sCmd = $aParams['command'];
+        $_bShowOutput = isset($aParams['output']) ? !!$aParams['output'] : true;
+
+        $_aRcOK = isset($aParams['exitOK']) ? $aParams['exitOK'] : [];
+        $_aRcWarning = isset($aParams['exitWarn']) ? $aParams['exitWarn'] : [];
+        $_aRcCritical = isset($aParams['exitCritical']) ? $aParams['exitCritical'] : [];
+
+        $_sMode = 'default';
+        if (count($_aRcOK) + count($_aRcWarning) + count($_aRcCritical)) {
+            $_sMode = 'exitcode';
+        }
+
+        exec($_sCmd, $aOutput, $iRc);
+        $_sOut = $_bShowOutput ? '<br>' . implode("<br>", $aOutput) : '';
+
+        switch ($_sMode) {
+            // non-zero exitcode is an error
+            case "default":
+                if ($iRc) {
+                    return [
+                        RESULT_ERROR,
+                        'command failed with exitcode ' . $iRc . ': [' . $_sCmd . ']' . $_sOut
+                    ];
+                } else {
+                    return [
+                        RESULT_OK,
+                        "OK [$_sCmd] $_sOut"
+                    ];
+                }
+                ;
+                // break;
+                ;
+
+            // handle given custom exitcodes
+            case "exitcode":
+                if (in_array($iRc, $_aRcCritical)) {
+                    return [
+                        RESULT_ERROR,
+                        "Critical exitcode $iRc detected: [$_sCmd] $_sOut"
+                    ];
+                }
+                if (in_array($iRc, $_aRcWarning)) {
+                    return [
+                        RESULT_WARNING,
+                        "Warning exitcode $iRc detected: [$_sCmd] $_sOut"
+                    ];
+                }
+                if ($iRc == 0 || in_array($iRc, $_aRcOK)) {
+                    return [
+                        RESULT_OK,
+                        "OK exitcode $iRc detected: [$_sCmd] $_sOut"
+                    ];
+                }
+                return [
+                    RESULT_UNKNOWN,
+                    "UNKNOWN - unhandled exitcode $iRc detected: [$_sCmd] $_sOut"
+                ];
+            case "search":
+                return [
+                    RESULT_UNKNOWN,
+                    "UNKNOWN method [$_sMode] - is not implemented yet."
+                ];
+                // break;
+                ;
+            default:
+                return [
+                    RESULT_UNKNOWN,
+                    'UNKNOWN mode [' . htmlentities($_sMode) . '].'
+                ];
+        } // switch($_sMode)
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/file.php b/public_html/appmonitor/plugins/checks/file.php
new file mode 100755
index 0000000000000000000000000000000000000000..f0aafde19c2330ced2a1d19762913db36e625b74
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/file.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CUSTOM CHECK FOR FILE OBJECTS
+ * 
+ * Check files, directories, links if the exist or not, if they are accessible 
+ * or not.
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-26  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ */
+class checkFile extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @param array   $aParams - see run() method
+     * @return string
+     */
+    public function getGroup(array $aParams = []): string
+    {
+        $sReturn = 'file';
+        if (isset($aParams['dir'])) {
+            $sReturn = 'folder';
+        }
+        foreach (['exists', 'executable', 'readable', 'writable'] as $sFlag) {
+            if (isset($aParams[$sFlag]) && !$aParams[$sFlag]) {
+                $sReturn = 'deny';
+            }
+        }
+        return $sReturn;
+    }
+
+    /**
+     * Check a file
+     * @param array $aParams
+     * [
+     *     "filename"    directory that must exist
+     *     "exists"      "filename" must exist/ must be absent
+     *     "dir"         filetype directory
+     *     "file"        filetype file
+     *     "link"        filetype symbolic link
+     *     "executable"  flag executable
+     *     "readable"    flag is readable
+     *     "writable"    flag is writable
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $aOK = [];
+        $aErrors = [];
+        $this->_checkArrayKeys($aParams, "filename");
+        $sFile = $aParams["filename"];
+
+        if (isset($aParams['exists'])) {
+            $sMyflag = 'exists=' . ($aParams['exists'] ? 'yes' : 'no');
+            if (file_exists($sFile) && $aParams['exists']) {
+                $aOK[] = $sMyflag;
+            } else {
+                $aErrors[] = $sMyflag;
+            }
+        }
+        foreach (['dir', 'executable', 'file', 'link', 'readable', 'writable'] as $sFiletest) {
+            if (isset($aParams[$sFiletest])) {
+                $sTestCmd = 'return is_' . $sFiletest . '("' . $sFile . '");';
+                if (eval ($sTestCmd) && $aParams[$sFiletest]) {
+                    $aOK[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no');
+                } else {
+                    $aErrors[] = $sFiletest . '=' . ($aParams[$sFiletest] ? 'yes' : 'no');
+                }
+            }
+        }
+        $sMessage = (count($aOK) ? ' flags OK: ' . implode('|', $aOK) : '')
+            . ' ' . (count($aErrors) ? ' flags FAILED: ' . implode('|', $aErrors) : '')
+        ;
+        if (count($aErrors)) {
+            return [
+                RESULT_ERROR,
+                "file test [$sFile] $sMessage"
+            ];
+        } else {
+            return [
+                RESULT_OK,
+                "file test [$sFile] $sMessage"
+            ];
+        }
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/hello.php b/public_html/appmonitor/plugins/checks/hello.php
new file mode 100755
index 0000000000000000000000000000000000000000..e2ba5e60f28924b57535c5f5da9c68ee8fed8354
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/hello.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * EXAMPLE CUSTOM CHECK THAT SENDS A HELLO
+ * 
+ * A plugin always is loaded in clases/appmonitor-checks.class.php
+ * Have look there for the used protected classes
+ * ____________________________________________________________________________
+ * 
+ * PARAMS:
+ *   message {string}  a custom message to display
+ * 
+ * USAGE:
+ * 
+ * $oMonitor->addCheck(
+ *     [
+ *         "name" => "hello plugin",
+ *         "description" => "test a plugin ... plugins/checkHello.php",
+ *         "check" => [
+ *             "function" => "Hello",
+ *             "params" => [
+ *                 "message" => "Here I am",
+ *             ],
+ *         ],
+ *     ]
+ * );
+ * ____________________________________________________________________________
+ * 
+ * 2019-06-05  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkHello extends appmonitorcheck
+{
+
+    /**
+     * Run the check
+     * @param array   $aParams
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+
+        // --- (1) verify if array key(s) exist:
+        $this->_checkArrayKeys($aParams, "message");
+
+
+        // --- (2) do something magic
+
+
+        // --- (3) response
+        // see method appmonitorcheck->_setReturn()
+        // 
+        // {integer} you should use a RESULT_XYZ constant:
+        //              RESULT_OK|RESULT_UNKNOWN|RESULT_WARNING|RESULT_ERROR
+        // {string}  output text 
+        // {array}   optional: counter data
+        //              type   => {string} "counter"
+        //              count  => {float}  value
+        //              visual => {string} one of bar|line|simple (+params)
+        //           
+        return [
+            RESULT_OK,
+            'Hello world! My message is: ' . $aParams['message']
+        ];
+    }
+}
diff --git a/public_html/appmonitor/plugins/checks/httpcontent.php b/public_html/appmonitor/plugins/checks/httpcontent.php
new file mode 100755
index 0000000000000000000000000000000000000000..3d981c54e7a34dcf29f9473681d7e2a2be8e950c
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/httpcontent.php
@@ -0,0 +1,251 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK RESPONSE OF AN HTTP REQUEST
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-26  <axel.hahn@iml.unibe.ch>
+ * 2022-12-21  <axel.hahn@unibe.ch>      add flag sslverify
+ * 2023-07-06  <axel.hahn@unibe.ch>      add flag userpwd
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 2024-11-22  <axel.hahn@unibe.ch>      Return unknown if curl module is not active
+ */
+class checkHttpContent extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * It is a "service" icon or "deny" for expected failures
+     * 
+     * @param array   $aParams with optional 'status' containing http response code
+     * @return string
+     */
+    public function getGroup(array $aParams=[]): string
+    {
+        $sReturn = 'service';
+        if (isset($aParams['status']) && $aParams['status'] > 300 && $aParams['status'] < 500) {
+            $sReturn = 'deny';
+        }
+        return $sReturn;
+    }
+
+    /**
+     * Make http request and test response header + body
+     * @param array $aParams
+     * [
+     *     url                 string   url to fetch
+     *     userpwd             string   set user and password; syntax: "[username]:[password]"
+     *     timeout             integer  optional timeout in sec; default: 5
+     *     headeronly          boolean  optional flag to fetch http response herader only; default: false = returns header and body
+     *     follow              boolean  optional flag to follow a location; default: false = do not follow
+     *     sslverify           boolean  flag: enable/ disable verification of ssl certificate; default: true (verification is on)
+     *
+     *     status              integer  test for an expected http status code; if none is given then test fails on status 400 and greater
+     *
+     *     headercontains      string   test for a string in the http response header; it returns OK if the text was found
+     *     headernotcontains   string   test for a string in the http response header; it returns OK if the text was not found
+     *     headerregex         string   test for a regex in the http response header; it returns OK if the regex matches; example: "headerregex"=>"/lowercasematch/i"
+     *
+     *     bodycontains        string   test for a string in the http response body; it returns OK if the text was found
+     *     bodynotcontains     string   test for a string in the http response body; it returns OK if the text was not found
+     *     bodyregex           string   test for a regex in the http response body; it returns OK if the regex matches; example: "headerregex"=>"/lowercasematch/i"
+     * ]
+     */
+    public function run(array $aParams)
+    {
+        $this->_checkArrayKeys($aParams, "url");
+        if (!function_exists("curl_init")) {
+            return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform mysqli test. The php-curl module is not active."];
+        }
+        $bShowContent = (isset($aParams["content"]) && $aParams["content"]) ? true : false;
+        $ch = curl_init($aParams["url"]);
+
+        curl_setopt($ch, CURLOPT_HEADER, 1);
+        curl_setopt($ch, CURLOPT_NOBODY, isset($aParams["headeronly"]) && $aParams["headeronly"]);
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, isset($aParams["follow"]) && $aParams["follow"]);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, isset($aParams["sslverify"]) ? !!$aParams["sslverify"] : 1);
+        curl_setopt($ch, CURLOPT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp);
+        if (isset($aParams["userpwd"])) {
+            curl_setopt($ch, CURLOPT_USERPWD, $aParams["userpwd"]);
+        }
+
+        $res = curl_exec($ch);
+
+        if (!$res) {
+            $iErrorCode = curl_errno($ch);
+            $sErrorMsg = curl_error($ch);
+            curl_close($ch);
+            return [
+                RESULT_ERROR,
+                'ERROR: failed to fetch ' . $aParams["url"] . ' - curl error #' . $iErrorCode . ': ' . $sErrorMsg
+            ];
+        }
+        $sOut = '';
+        $bError = false;
+
+        $aInfos = curl_getinfo($ch);
+        /*
+            Array
+            (
+                [url] => https://www.iml.unibe.ch/
+                [content_type] => text/html; charset=utf-8
+                [http_code] => 200
+                [header_size] => 926
+                [request_size] => 55
+                [filetime] => -1
+                [ssl_verify_result] => 20
+                [redirect_count] => 0
+                [total_time] => 1.812
+                [namelookup_time] => 0
+                [connect_time] => 0
+                [pretransfer_time] => 0.015
+                [size_upload] => 0
+                [size_download] => 94654
+                [speed_download] => 52237
+                [speed_upload] => 0
+                [download_content_length] => -1
+                [upload_content_length] => -1
+                [starttransfer_time] => 1.812
+                [redirect_time] => 0
+                [redirect_url] => 
+                [primary_ip] => 130.92.30.80
+                [certinfo] => Array
+                    (
+                    )
+
+                [primary_port] => 443
+                [local_ip] => 10.1.30.49
+                [local_port] => 63597
+            )
+         */
+
+        curl_close($ch);
+
+        $aTmp = explode("\r\n\r\n", $res, 2);
+        $sHttpHeader = $aTmp[0];
+        $sHttpBody = $aTmp[1] ?? false;
+
+        // ---------- check functions
+
+        // --- http status code
+        $sOut .= "Http status: " . $aInfos['http_code'] . " - ";
+        if (isset($aParams["status"])) {
+            if ($aInfos['http_code'] === $aParams["status"]) {
+                $sOut .= "compare OK<br>";
+            } else {
+                $sOut .= "compare failed<br>";
+                $bError = true;
+            }
+        } else {
+            if ($aInfos['http_code'] >= 400) {
+                $sOut .= "Error page detected<br>";
+                $bError = true;
+            } else {
+                $sOut .= "request successful<br>";
+            }
+        }
+        // --- http header
+        if (isset($aParams["headercontains"]) && $aParams["headercontains"]) {
+            $sOut .= "Http header contains &quot;" . $aParams["headercontains"] . "&quot; - ";
+            if (!strstr($sHttpHeader, $aParams["headercontains"]) === false) {
+                $sOut .= "compare OK<br>";
+            } else {
+                $sOut .= "compare failed<br>";
+                $bError = true;
+            }
+        }
+        if (isset($aParams["headernotcontains"]) && $aParams["headernotcontains"]) {
+            $sOut .= "Http header does not contain &quot;" . $aParams["headernotcontains"] . "&quot; - ";
+            if (strstr($sHttpHeader, $aParams["headernotcontains"]) === false) {
+                $sOut .= "compare OK<br>";
+            } else {
+                $sOut .= "compare failed<br>";
+                $bError = true;
+            }
+        }
+        if (isset($aParams["headerregex"]) && $aParams["headerregex"]) {
+            $sOut .= "Http header regex test &quot;" . $aParams["headerregex"] . "&quot; - ";
+            try {
+                $bRegex = preg_match($aParams["headerregex"], $sHttpHeader);
+                if ($bRegex) {
+                    $sOut .= "compare OK<br>";
+                } else {
+                    $sOut .= "compare failed<br>";
+                    $bError = true;
+                }
+            } catch (Exception $e) {
+                $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>';
+                $bError = true;
+            }
+        }
+        // --- http body
+        if (isset($aParams["bodycontains"]) && $aParams["bodycontains"]) {
+            $sOut .= "Http body contains &quot;" . $aParams["bodycontains"] . "&quot; - ";
+            if (!strstr($sHttpBody, $aParams["bodycontains"]) === false) {
+                $sOut .= "compare OK<br>";
+            } else {
+                $sOut .= "compare failed<br>";
+                $bError = true;
+            }
+        }
+        if (isset($aParams["bodynotcontains"]) && $aParams["bodynotcontains"]) {
+            $sOut .= "Http body does not contain &quot;" . $aParams["bodynotcontains"] . "&quot; - ";
+            if (strstr($sHttpBody, $aParams["bodynotcontains"]) === false) {
+                $sOut .= "compare OK<br>";
+            } else {
+                $sOut .= "compare failed<br>";
+                $bError = true;
+            }
+        }
+        if (isset($aParams["bodyregex"]) && $aParams["bodyregex"]) {
+            $sOut .= "Http body regex test &quot;" . $aParams["bodyregex"] . "&quot; - ";
+            try {
+                $bRegex = preg_match($aParams["bodyregex"], $sHttpBody);
+                if ($bRegex) {
+                    $sOut .= "compare OK<br>";
+                } else {
+                    $sOut .= "compare failed<br>";
+                    $bError = true;
+                }
+            } catch (Exception $e) {
+                $sOut .= "Wrong REGEX<br>" . print_r($e, 1) . '<br>';
+                $bError = true;
+            }
+        }
+
+        if (!$bError) {
+            return [
+                RESULT_OK,
+                'OK: http check "' . $aParams["url"] . '".<br>' . $sOut
+            ];
+        } else {
+            return [
+                RESULT_ERROR,
+                'ERROR: http check "' . $aParams["url"] . '".<br>' . $sOut
+            ];
+        }
+
+        /*
+        echo '<pre>'; 
+        echo $sOut."<hr>";
+        echo "<hr>HEADER: ".htmlentities($sHttpHeader)."<hr>";
+        print_r($aParams); print_r($aInfos); 
+        // echo htmlentities($sHttpBody);
+        die();
+         */
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/loadmeter.php b/public_html/appmonitor/plugins/checks/loadmeter.php
new file mode 100755
index 0000000000000000000000000000000000000000..13911af15f71926e01f23cce97154217fe076ccd
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/loadmeter.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * SHOW LOAD AS LINE
+ * 
+ * A plugin always is loaded in clases/appmonitor-checks.class.php
+ * Have look there for the used protected classes
+ * ____________________________________________________________________________
+ * 
+ * PARAMS:
+ *   warning {float}  limit to switch to warning
+ *   error   {float}  limit to switch to error
+ * 
+ * USAGE:
+ * 
+ * $oMonitor->addCheck(
+ *     [
+ *         "name" => "plugin Load",
+ *         "description" => "check current load",
+ *         "check" => [
+ *             "function" => "Loadmeter",
+ *             "params" => [
+ *                "warning" => 1.0,
+ *                "error" => 3,
+ *             ],
+ *         ],
+ *         "worstresult" => RESULT_OK
+ *     ]
+ * );
+ * ____________________________________________________________________________
+ * 
+ * 2019-06-06  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 2024-07-25  <axel.hahn@unibe.ch>      float return with 2 digits behind comma
+ * 
+ */
+class checkLoadmeter extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'monitor';
+    }
+
+    /**
+     * Detect load of a machine and return a float value
+     * windows part was taken from https://stackoverflow.com/questions/5588616/how-do-you-calculate-server-load-in-php
+     * @return float
+     */
+    protected function _getLoad(): float
+    {
+        if (function_exists('sys_getloadavg')) {
+            $load = sys_getloadavg();
+            return $load[0];
+        } else {
+            // Only MS Windows has not implemented sys_getloadavg
+            // try something else
+            if (class_exists('COM')) {
+                $wmi = new COM('WinMgmts:\\\\.');
+                $cpus = $wmi->InstancesOf('Win32_Processor');
+                $load = 0;
+                if (version_compare('4.50.0', PHP_VERSION) == 1) {
+                    while ($cpu = $cpus->Next()) {
+                        $load += $cpu->LoadPercentage;
+                    }
+                } else {
+                    foreach ($cpus as $cpu) {
+                        $load += $cpu->LoadPercentage;
+                    }
+                }
+                return $load;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Run the check and get load
+     * @param array   $aParams  optional array with keys warning,error
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+
+        // --- (1) verify if array key(s) exist:
+        // $this->_checkArrayKeys($aParams, "...");
+
+        // --- (2) do something magic
+        // $fLoad=rand(0, 1.3);
+        // $fLoad=$this->_getServerLoad();
+        $fLoad = $this->_getLoad();
+
+        // set result code
+        if ($fLoad === false) {
+            $iResult = RESULT_UNKNOWN;
+        } else {
+            $iResult = RESULT_OK;
+            if (isset($aParams['warning']) && $aParams['warning'] && $fLoad > $aParams['warning']) {
+                $iResult = RESULT_WARNING;
+            }
+            if (isset($aParams['error']) && $aParams['error'] && $fLoad > $aParams['error']) {
+                $iResult = RESULT_ERROR;
+            }
+        }
+
+
+        // --- (3) response
+        // see method appmonitorcheck->_setReturn()
+        // 
+        // {integer} you should use a RESULT_XYZ constant:
+        //              RESULT_OK|RESULT_UNKNOWN|RESULT_WARNING|RESULT_ERROR
+        // {string}  output text 
+        // {array}   optional: counter data
+        //              type   => {string} "counter"
+        //              count  => {float}  value
+        //              visual => {string} one of bar|line|simple (+params)
+        //           
+        return [
+            $iResult,
+            ($fLoad === false ? 'load value is not available' : 'current load is: ' . round($fLoad, 2)),
+            ($fLoad === false
+                ? []
+                : [
+                    'type' => 'counter',
+                    'count' => round($fLoad, 2),
+                    'visual' => 'line',
+                ]
+            )
+        ]
+        ;
+    }
+}
diff --git a/public_html/appmonitor/plugins/checks/mysqlconnect.php b/public_html/appmonitor/plugins/checks/mysqlconnect.php
new file mode 100755
index 0000000000000000000000000000000000000000..5fe8a611cccb570bdd57475a915b11a9f16c96eb
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/mysqlconnect.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK DATABASE CONNECTION WITH MYSQLI
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-27  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 2024-11-22  <axel.hahn@unibe.ch>      detect installed mysqli function
+ */
+class checkMysqlConnect extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'database';
+    }
+
+    /**
+     * Check mysql connection to a database using mysqli realconnect
+     * @param array $aParams
+     * [
+     *     server              string   database hostname / ip address
+     *     user                string   db user
+     *     password            string   password for db user
+     *     db                  string   schema / database name
+     *     port                integer  optional: port
+     *     timeout             integer  optional timeout in sec; default: 5
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $this->_checkArrayKeys($aParams, "server,user,password,db");
+        if (!function_exists("mysqli_init")) {
+            return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform mysqli test. The php-mysqli module is not active."];
+        }
+        $mysqli = mysqli_init();
+        if (!$mysqli) {
+            return [RESULT_ERROR, 'ERROR: mysqli_init failed.'];
+        }
+        if (!$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp)) {
+            return [RESULT_ERROR, 'ERROR: setting mysqli_options failed.'];
+        }
+
+        $db = (isset($aParams["port"]) && $aParams["port"])
+            ? $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"], $aParams["port"])
+            : $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"])
+        ;
+        if ($db) {
+            $mysqli->close();
+            return [RESULT_OK, "OK: Mysql database " . $aParams["db"] . " was connected"];
+        } else {
+            return [
+                RESULT_ERROR,
+                "ERROR: Mysql database " . $aParams["db"] . " was not connected. Error " . mysqli_connect_errno() . ": " . mysqli_connect_error()
+            ];
+        }
+    }
+}
diff --git a/public_html/appmonitor/plugins/checks/pdoconnect.php b/public_html/appmonitor/plugins/checks/pdoconnect.php
new file mode 100755
index 0000000000000000000000000000000000000000..d95b3d55074553181d307f68e59bdcfcaba9ab1f
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/pdoconnect.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK DATABASE CONNECTION USING PDO
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-27  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkPdoConnect extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'database';
+    }
+    /**
+     * Check connection to a database using pdo
+     * see http://php.net/manual/en/pdo.drivers.php
+     * 
+     * @param array $aParams
+     * [
+     *     connect             string   connect string
+     *     user                string   db user
+     *     password            string   password for db user
+     *     timeout             integer  optional timeout in sec; default: 5
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $this->_checkArrayKeys($aParams, "connect,user,password");
+
+        try {
+            $db = new PDO(
+                $aParams['connect'],
+                $aParams['user'],
+                $aParams['password'],
+                [
+                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+
+                    // timeout
+                    // Not all drivers support this option; mysqli does
+                    PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp,
+                    // mssql
+                    // PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $this->_iTimeoutTcp,  
+                ]
+            );
+            $db = null;
+            return [RESULT_OK, "OK: Database was connected with PDO " . $aParams['connect']];
+        } catch (PDOException $e) {
+            return [RESULT_ERROR, "ERROR: Database was not connected " . $aParams['connect'] . " was not connected. Error " . $e->getMessage()];
+        }
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/phpmodules.php b/public_html/appmonitor/plugins/checks/phpmodules.php
new file mode 100644
index 0000000000000000000000000000000000000000..b4b8170aa02b816070867397ddc884bfa3dc1557
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/phpmodules.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK IF NEEDED PHP MODULES ARE INSTALLED
+ * ____________________________________________________________________________
+ * 
+ * 2022-05-06  <axel.hahn@iml.unibe.ch>  first lines
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkPhpmodules extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'service';
+    }
+
+    /**
+     * Check if system is listening to a given port
+     * @param array $aParams
+     * [
+     *     required     array  list of required php modules
+     *     optional     array  optional: list of optional php modules
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $sOut = '';
+        $bHasError = false;
+        $bHasWarning = false;
+        // $this->_checkArrayKeys($aParams, "required");
+
+        // --- get all modules
+        $aAllMods = get_loaded_extensions(false);
+
+        // --- check required modules
+        if (isset($aParams['required']) && count($aParams['required'])) {
+            $sOut .= 'Required: ';
+            foreach ($aParams['required'] as $sMod) {
+                $sOut .= $sMod . '=';
+                if (!array_search($sMod, $aAllMods) === false) {
+                    $sOut .= 'OK;';
+                } else {
+                    $bHasError = true;
+                    $sOut .= 'MISS;';
+                }
+            }
+        }
+        // --- check optional modules
+        if (isset($aParams['optional']) && count($aParams['optional'])) {
+            $sOut .= ($sOut ? '|' : '') . 'Optional: ';
+            foreach ($aParams['optional'] as $sMod) {
+                $sOut .= $sMod . '=';
+                if (!array_search($sMod, $aAllMods) === false) {
+                    $sOut .= 'OK;';
+                } else {
+                    $bHasWarning = true;
+                    $sOut .= 'MISS;';
+                }
+            }
+        }
+
+        // --- return result
+        if ($bHasError) {
+            return [RESULT_ERROR, "ERROR: " . $sOut];
+        }
+        if ($bHasWarning) {
+            return [RESULT_WARNING, "WARNING: " . $sOut];
+        }
+        return [RESULT_OK, "OK: " . $sOut];
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/ping.php b/public_html/appmonitor/plugins/checks/ping.php
new file mode 100644
index 0000000000000000000000000000000000000000..3056314ccbedca581d82c8ae204305046aa37130
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/ping.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK PING RESPONSE TIME VIA ICMP
+ * ____________________________________________________________________________
+ * 
+ * 2022-07-05  <axel.hahn@iml.unibe.ch>
+ * 2022-09-16  <axel.hahn@iml.unibe.ch>  read error before closing socket.
+ * 2022-11-22  <axel.hahn@iml.unibe.ch>  Use exec with detecting MS Win for the ping parameter for count of pings
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ */
+class checkPing extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'network';
+    }
+
+    /**
+     * Check ping to a target
+     * @param array $aParams
+     * [
+     *     host                string   optional hostname to connect; default: 127.0.0.1
+     *     timeout             integer  OBSOLET (because using exec): optional timeout in sec; default: 5
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $sHost = $aParams['host'] ?? '127.0.0.1';
+
+        $sParamCount = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? "n" : "c";
+        $iRepeat = 1;
+
+        $sCommand = "ping -$sParamCount $iRepeat $sHost 2>&1";
+        exec($sCommand, $aOut, $iRc);
+        $sOut = implode("\n", $aOut);
+
+        if ($iRc > 0) {
+            return [RESULT_ERROR, "ERROR: ping to $sHost failed.\n" . $sOut];
+        }
+        return [RESULT_OK, "OK: ping to $sHost\n" . $sOut];
+
+        /*
+            Socket functions require root :-/
+
+        if (!function_exists('socket_create')){
+            return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform ping test. The socket module is not enabled in the php installation."];
+        }
+
+        // ICMP ping packet with a pre-calculated checksum
+        $package = "\x08\x00\x7d\x4b\x00\x00\x00\x00PingHost";
+        $socket  = socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp'));
+        // TODO
+        if(!$socket){
+                die("ERROR: unable to create socket");
+        }
+        socket_set_option(
+            $socket, 
+            SOL_SOCKET, 
+            SO_RCVTIMEO, 
+            [
+                "sec"=>(isset($aParams["timeout"]) && (int)$aParams["timeout"]) ? (int)$aParams["timeout"] : $this->_iTimeoutTcp, // timeout in seconds
+                "usec"=>0
+            ]
+        );
+
+        $start = microtime(true);
+        socket_connect($socket, $sHost, 0);
+        $connect = socket_send($socket, $package, strLen($package), 0);
+        if($connect){
+            if (socket_read($socket, 255)){
+                $result = microtime(true) - $start;
+                socket_close($socket);
+                return [RESULT_OK, 
+                    "OK: ping to $sHost",
+                    [
+                        'type'=>'counter',
+                        'count'=>$result,
+                        'visual'=>'line',
+                    ]
+
+                ];
+            } else {
+                $aResult=[RESULT_ERROR, "ERROR: ping to $sHost failed after connect." . socket_strerror(socket_last_error($socket))];
+                socket_close($socket);
+                return $aResult;
+            }
+        } else {
+            return [RESULT_ERROR, "ERROR: ping to $sHost failed. " . socket_strerror(socket_last_error($socket))];
+        }
+
+        */
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/porttcp.php b/public_html/appmonitor/plugins/checks/porttcp.php
new file mode 100755
index 0000000000000000000000000000000000000000..bd47ce5fbdf5919891c239debe68887a6b6b87da
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/porttcp.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK TCP CONNECTION TO A GIVEN PORT
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-27  <axel.hahn@iml.unibe.ch>
+ * 2022-07-05  <axel.hahn@iml.unibe.ch>  send unknown if socket module is not activated.
+ * 2022-09-16  <axel.hahn@iml.unibe.ch>  read error before closing socket.
+ * 2022-12-05  <axel.hahn@unibe.ch>      add @ sign at socket functions to prevent warning
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkPortTcp extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup(): string
+    {
+        return 'network';
+    }
+
+    /**
+     * Check if system is listening to a given port
+     * @param array $aParams
+     * [
+     *     port                integer  port
+     *     host                string   optional hostname to connect; default: 127.0.0.1
+     *     timeout             integer  optional timeout in sec; default: 5
+     * ]
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $this->_checkArrayKeys($aParams, "port");
+
+        $sHost = $aParams['host'] ?? '127.0.0.1';
+        $iPort = (int) $aParams['port'];
+
+        if (!function_exists('socket_create')) {
+            return [RESULT_UNKNOWN, "UNKNOWN: Unable to perform tcp test. The php-sockets module is not enabled in the php installation."];
+        }
+
+        // from http://php.net/manual/de/sockets.examples.php
+
+        $socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+        if ($socket === false) {
+            return [RESULT_UNKNOWN, "ERROR: $sHost:$iPort was not checked. socket_create() failed: " . socket_strerror(socket_last_error())];
+        }
+        // set socket timeout
+        socket_set_option(
+            $socket,
+            SOL_SOCKET,  // socket level
+            SO_SNDTIMEO, // timeout option
+            [
+                "sec" => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp, // timeout in seconds
+                "usec" => 0
+            ]
+        );
+
+        $result = @socket_connect($socket, $sHost, $iPort);
+        if ($result === false) {
+            $aResult = [RESULT_ERROR, "ERROR: $sHost:$iPort failed. " . socket_strerror(socket_last_error($socket))];
+            socket_close($socket);
+            return $aResult;
+        } else {
+            socket_close($socket);
+            return [RESULT_OK, "OK: $sHost:$iPort was connected."];
+        }
+    }
+
+}
diff --git a/public_html/appmonitor/plugins/checks/simple.php b/public_html/appmonitor/plugins/checks/simple.php
new file mode 100755
index 0000000000000000000000000000000000000000..3af69b69008682e7a91ed9cfe2be93ebc27b30ed
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/simple.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * SIMPLE CHECK
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-27  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkSimple extends appmonitorcheck
+{
+
+    /**
+     * Most simple check: set given values
+     * Use this function to add a counter
+     * 
+     * @param array $aParams
+     * array keys:
+     *     value               string   description text
+     *     result              integer  RESULT_nn
+     * 
+     *     brainstorming for a future release
+     * 
+     *     "counter"  optioal: array of counter values
+     *         - label         string   a label
+     *         - value         float    a number
+     *         - type          string   one of simple | bar | line
+     * @return array
+     */
+    public function run(array $aParams): array
+    {
+        $this->_checkArrayKeys($aParams, "result,value");
+        // $this->_setReturn((int) $aParams["result"], $aParams["value"]);
+        $aData = [];
+        foreach (['type', 'count', 'visual'] as $sMyKey) {
+            if (isset($aParams[$sMyKey])) {
+                $aData[$sMyKey] = $aParams[$sMyKey];
+            }
+        }
+        return [
+            $aParams["result"],
+            $aParams["value"],
+            count($aData) ? $aData : false
+        ];
+    }
+}
diff --git a/public_html/appmonitor/plugins/checks/sqliteconnect.php b/public_html/appmonitor/plugins/checks/sqliteconnect.php
new file mode 100755
index 0000000000000000000000000000000000000000..703db60d7467d1ff6968b659e0d2c3ca8c2d5c22
--- /dev/null
+++ b/public_html/appmonitor/plugins/checks/sqliteconnect.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * CHECK SWLITE CONNECTION USING PDO
+ * ____________________________________________________________________________
+ * 
+ * 2021-10-27  <axel.hahn@iml.unibe.ch>
+ * 2024-07-23  <axel.hahn@unibe.ch>      php 8 only: use typed variables
+ * 
+ */
+class checkSqliteConnect extends appmonitorcheck
+{
+    /**
+     * Get default group of this check
+     * @return string
+     */
+    public function getGroup()
+    {
+        return 'database';
+    }
+
+    /**
+     * check sqlite connection
+     * @param array $aParams
+     * [
+     *     db                  string   full path of sqlite file 
+     *     timeout             integer  optional timeout in sec; default: 5
+     * ]
+     * @return array
+     */
+    public function run($aParams): array
+    {
+        $this->_checkArrayKeys($aParams, "db");
+        if (!file_exists($aParams["db"])) {
+            return [
+                RESULT_ERROR,
+                "ERROR: Sqlite database file " . $aParams["db"] . " does not exist."
+            ];
+        }
+        if (!isset($aParams['user'])) {
+            $aParams['user'] = '';
+        }
+        if (!isset($aParams['password'])) {
+            $aParams['password'] = '';
+        }
+        try {
+            // $db = new SQLite3($sqliteDB);
+            // $db = new PDO("sqlite:".$sqliteDB);
+            $o = new PDO(
+                "sqlite:" . $aParams["db"],
+                $aParams['user'],
+                $aParams['password'],
+                [
+                    PDO::ATTR_TIMEOUT => (isset($aParams["timeout"]) && (int) $aParams["timeout"]) ? (int) $aParams["timeout"] : $this->_iTimeoutTcp,
+                ]
+            );
+            return [
+                RESULT_OK,
+                "OK: Sqlite database " . $aParams["db"] . " was connected"
+            ];
+        } catch (Exception $e) {
+            return [
+                RESULT_ERROR, 
+                "ERROR: Sqlite database " . $aParams["db"] . " was not connected. " . $e->getMessage()
+            ];
+        }
+    }
+
+}
diff --git a/public_html/appmonitor/tests/test_plugins.php b/public_html/appmonitor/tests/test_plugins.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd62fa4f6eb962052d989d27840f381d2493e6f6
--- /dev/null
+++ b/public_html/appmonitor/tests/test_plugins.php
@@ -0,0 +1,48 @@
+<?php
+/*
+    TEST CLIENT CHECKS
+*/
+
+// ----------------------------------------------------------------------
+// INIT
+$sApproot = str_replace('\\', '/', dirname(__DIR__));
+
+
+// require_once(__DIR__.'/../classes/client_all_in_one.php');
+// echo "OK: file client_all_in_one.php was loaded\n";
+
+require_once(__DIR__.'/../classes/appmonitor-client.class.php');
+
+$oMonitor = new appmonitor();
+
+$oMonitor->listChecks();
+
+
+echo "OK: class appmonitor was initialized\n";
+
+
+// ----------------------------------------------------------------------
+$oMonitor->addTag('monitoring');
+
+// ----------------------------------------------------------------------
+$oMonitor->addCheck(
+    [
+        "name" => "check config subdir",
+        "description" => "Check config target directory",
+        "check" => [
+            "function" => "File",
+            "params" => [
+                "filename" => $sApproot . "/server/config",
+                "dir" => true,
+                "writable" => true,
+            ],
+        ],
+    ]
+);
+echo "OK: the plugin File check was added.\n";
+
+// ----------------------------------------------------------------------
+$oMonitor->setResult();
+echo "OK: setResult() was executed.\n";
+
+// ----------------------------------------------------------------------