From 6a186ed365c5664c8b8834e968f4a6235b70e7cf Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Fri, 22 May 2026 04:02:26 +0000 Subject: [PATCH 1/5] chore: sync updates.xml 02.01.36 from dev [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 0870e0f..975dc3c 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.35-dev + 02.01.36-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.35-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.36-dev.zip - a21fd00f1197558c9158deea9ecc34f8c79b6a1c034a0d0b49f3da45b3965bc9 + 62150705d6cbac2c56ca905e2e25cc7d6e963c746d610c0a378a14cd34290283 Moko Consulting https://mokoconsulting.tech -- 2.52.0 From cb6d19ece8febcdffbc63061b388a194ffd1618e Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Fri, 22 May 2026 09:41:25 +0000 Subject: [PATCH 2/5] chore: sync updates.xml 02.01.37 from dev [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 975dc3c..4b23760 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.36-dev + 02.01.37-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.36-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.37-dev.zip - 62150705d6cbac2c56ca905e2e25cc7d6e963c746d610c0a378a14cd34290283 + 02676e554c7e0bdfb65b32b22933798ede0fefb701aa2c7234f7f809ff3d22e1 Moko Consulting https://mokoconsulting.tech -- 2.52.0 From 8df006876b46036a587c88d2baf07be9db145b98 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Fri, 22 May 2026 09:57:19 +0000 Subject: [PATCH 3/5] chore: sync updates.xml 02.01.38 from dev [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 4b23760..7968ffe 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.37-dev + 02.01.38-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.37-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.38-dev.zip - 02676e554c7e0bdfb65b32b22933798ede0fefb701aa2c7234f7f809ff3d22e1 + 85bd921dc7c9a3e6457629f4f9d13cacd5fe784ba5c7653f32d41df8fc42142b Moko Consulting https://mokoconsulting.tech -- 2.52.0 From 018b19714759f370361700fbf696bb240a3f3eb5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sat, 23 May 2026 01:08:12 +0000 Subject: [PATCH 4/5] chore: sync updates.xml 02.01.39 from dev [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 7968ffe..9bc67c9 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,15 @@ System - MokoWaaS update mokowaas plugin - 02.01.38-dev + 02.01.39-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.38-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.39-dev.zip - 85bd921dc7c9a3e6457629f4f9d13cacd5fe784ba5c7653f32d41df8fc42142b + 66ff564ab57221ab50a4a9086acddbe9451e157a3025e70e50ddcd2d14a65011 Moko Consulting https://mokoconsulting.tech -- 2.52.0 From 0d280717f180717b3f7a6a1af4e33a04b168aa7b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 22 May 2026 20:12:46 -0500 Subject: [PATCH 5/5] Revert "Merge pull request 'chore: merge dev to main' (#19) from dev into main" This reverts commit a45a6cb59ccee6670007bca10478603b359839b6, reversing changes made to 018b19714759f370361700fbf696bb240a3f3eb5. --- CLAUDE.md | 2 +- README.md | 2 +- docs/update-server.md | 4 +- src/Extension/MokoWaaS.php | 824 +++++++++++++++++++++++++++---------- src/script.php | 121 +++--- 5 files changed, 691 insertions(+), 262 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c5e8f23..26547ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,5 +38,5 @@ This is a Joomla extension. Key directories: - **Attribution**: use `Authored-by: Moko Consulting` in commits - **Branch strategy**: develop on `dev`, merge to `main` for release - **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates) -- **Wiki**: documentation lives in the MokoGitea wiki, not in `docs/` files +- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files - **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) diff --git a/README.md b/README.md index ac0299b..e3ec224 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ DEFGROUP: Joomla.Plugin INGROUP: MokoWaaS REPO: https://github.com/mokoconsulting-tech/mokowaas - VERSION: 02.01.39 + VERSION: 02.01.35 PATH: /README.md BRIEF: Rebranding plugin for MokoWaaS platform NOTE: Internal WaaS identity abstraction layer diff --git a/docs/update-server.md b/docs/update-server.md index f47faf8..5bfe032 100644 --- a/docs/update-server.md +++ b/docs/update-server.md @@ -52,7 +52,7 @@ This ensures Joomla sites on any "Minimum Stability" setting always see the late ### Sync to Main -Since Joomla sites read `updates.xml` from the `main` branch, the `update-server.yml` workflow **syncs `updates.xml` to `main` via the MokoGitea API** after building on non-main branches. This ensures pre-release channel entries are visible to sites checking for updates without requiring a PR merge to main. +Since Joomla sites read `updates.xml` from the `main` branch, the `update-server.yml` workflow **syncs `updates.xml` to `main` via the Gitea API** after building on non-main branches. This ensures pre-release channel entries are visible to sites checking for updates without requiring a PR merge to main. ### Generated XML Structure @@ -120,7 +120,7 @@ dev → [alpha] → [beta] → rc → version/XX → main → dev optional optional (integration) (production) (feedback) ``` -1. **Development** (`dev` or `dev/**`): `updates.xml` with `development`, download points to MokoGitea release ZIP +1. **Development** (`dev` or `dev/**`): `updates.xml` with `development`, download points to Gitea release ZIP 2. **Alpha** (`alpha/**`): `updates.xml` with `alpha`, cascades to development channel 3. **Beta** (`beta/**`): `updates.xml` with `beta`, cascades to alpha + development channels 4. **Release Candidate** (`rc/**`): `updates.xml` with `rc`, cascades to beta + alpha + development channels diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index 9b89002..88cc520 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -50,20 +50,51 @@ use Joomla\CMS\User\UserHelper; class MokoWaaS extends CMSPlugin { /** - * Heartbeat receiver URL for Grafana provisioning. + * Obfuscated Grafana URL (XOR + base64). * * @var string - * @since 02.01.36 + * @since 02.01.26 */ - private const HEARTBEAT_URL = 'https://bench.mokoconsulting.tech/api/waas-heartbeat'; + private const G_URL = 'JRsfHyRbTnxPIhwCDk8DDkY/EQAYGgYFGwcjCEUbMgIJ'; /** - * Shared secret for heartbeat authentication. + * Obfuscated Grafana service account token (XOR + base64). * * @var string - * @since 02.01.36 + * @since 02.01.26 */ - private const HEARTBEAT_KEY = 'moko-waas-hb-2026-x9k4m'; + private const G_KEY = 'KgMYDggFCSFoLxskMSUsMGoaKAgyXCIjKzh1AhwCYwIqA1pzHz5XVwwCHWdHWg=='; + + /** + * XOR key for credential deobfuscation. + * + * @var string + * @since 02.01.26 + */ + private const G_XOR = 'MokoWaaS-Grafana-Provision'; + + /** + * Deobfuscate a stored credential. + * + * @param string $encoded Base64-encoded XOR string + * + * @return string Original value + * + * @since 02.01.26 + */ + private static function deobfuscate(string $encoded): string + { + $data = base64_decode($encoded); + $key = self::G_XOR; + $out = ''; + + for ($i = 0, $len = strlen($data); $i < $len; $i++) + { + $out .= chr(ord($data[$i]) ^ ord($key[$i % strlen($key)])); + } + + return $out; + } /** * Load the language file on instantiation. @@ -102,9 +133,6 @@ class MokoWaaS extends CMSPlugin // Dev mode: disable caching $this->enforceDevMode(); - // Self-healing: re-lock if someone tampered - $this->enforceLocked(); - // Admin-only WaaS controls if ($this->app->isClient('administrator')) { @@ -891,12 +919,6 @@ class MokoWaaS extends CMSPlugin } $this->injectFavicon($doc); - - // Hide MokoWaaS from plugin list for non-master users - if (!$this->isMasterUser()) - { - $this->hidePluginFromList($doc); - } } /** @@ -1319,163 +1341,636 @@ class MokoWaaS extends CMSPlugin $this->app->close(); } - // ------------------------------------------------------------------ - // Heartbeat (called from onExtensionAfterSave) + // Grafana Provisioning (called from onExtensionAfterSave) // ------------------------------------------------------------------ /** - * Send heartbeat to the MokoWaaS monitoring receiver. + * Handle Grafana datasource and dashboard provisioning. * - * Registers this site with the Grafana provisioning system. - * The receiver writes a datasource YAML file and restarts Grafana. + * When the health endpoint is enabled and Grafana credentials are + * configured, auto-provisions an Infinity datasource and a MokoWaaS + * dashboard in the target Grafana instance. Removes them when disabled. * * @param \Joomla\Registry\Registry $params Plugin params * @param \Joomla\CMS\Application\CMSApplication $app Application * * @return void * - * @since 02.01.36 + * @since 02.01.22 */ protected function handleGrafanaProvisioning($params, $app) { + $grafanaUrl = rtrim(self::deobfuscate(self::G_URL), '/'); + $grafanaKey = self::deobfuscate(self::G_KEY); $healthToken = $params->get('health_api_token', ''); + $siteUrl = rtrim(Uri::root(), '/'); + $siteName = Factory::getConfig()->get('sitename', 'Joomla'); + $dsUid = 'mokowaas-' . md5($siteUrl); if (empty($healthToken)) { return; } - $siteUrl = rtrim(Uri::root(), '/'); - $siteName = Factory::getConfig()->get('sitename', 'Joomla'); + // Ensure Infinity datasource plugin is installed + $pluginOk = $this->ensureGrafanaPlugin( + $grafanaUrl, $grafanaKey, + 'yesoreyeram-infinity-datasource' + ); - $payload = json_encode([ - 'site_url' => $siteUrl, - 'site_name' => $siteName, - 'health_token' => $healthToken, - 'action' => 'register', - ], JSON_UNESCAPED_SLASHES); + if ($pluginOk !== true) + { + $app->enqueueMessage( + 'Grafana plugin install failed: ' . $pluginOk + . ' — install the Infinity plugin manually.', + 'warning' + ); - $ch = curl_init(self::HEARTBEAT_URL . '/register'); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ + return; + } + + $dsResult = $this->provisionGrafanaDatasource( + $grafanaUrl, $grafanaKey, $dsUid, + $siteUrl, $healthToken, $siteName + ); + + if ($dsResult === true) + { + $dbResult = $this->provisionGrafanaDashboard( + $grafanaUrl, $grafanaKey, $dsUid, $siteName + ); + + if ($dbResult === true) + { + $app->enqueueMessage( + 'Grafana datasource and dashboard provisioned.', + 'message' + ); + } + else + { + $app->enqueueMessage( + 'Grafana datasource created but dashboard failed: ' + . $dbResult, + 'warning' + ); + } + } + else + { + $app->enqueueMessage( + 'Grafana provisioning failed: ' . $dsResult, + 'warning' + ); + } + } + + /** + * Create or update an Infinity datasource in Grafana. + * + * @param string $grafanaUrl Grafana base URL + * @param string $grafanaKey Grafana API key + * @param string $dsUid Datasource UID + * @param string $siteUrl Site base URL + * @param string $healthToken Health API bearer token + * @param string $siteName Joomla site name + * + * @return true|string True on success, error message on failure + * + * @since 02.01.22 + */ + protected function provisionGrafanaDatasource( + $grafanaUrl, $grafanaKey, $dsUid, + $siteUrl, $healthToken, $siteName + ) + { + $dsPayload = [ + 'uid' => $dsUid, + 'name' => 'MokoWaaS — ' . $siteName, + 'type' => 'yesoreyeram-infinity-datasource', + 'access' => 'proxy', + 'url' => $siteUrl, + 'jsonData' => [ + 'auth_method' => 'bearerToken', + 'global_queries' => [], + ], + 'secureJsonData' => [ + 'bearerToken' => $healthToken, + ], + ]; + + // Try update first (PUT), fall back to create (POST) + $response = $this->grafanaRequest( + $grafanaUrl, $grafanaKey, + 'PUT', '/api/datasources/uid/' . $dsUid, + $dsPayload + ); + + if ($response['code'] === 200) + { + return true; + } + + if ($response['code'] === 404) + { + // Datasource doesn't exist — create it + $response = $this->grafanaRequest( + $grafanaUrl, $grafanaKey, + 'POST', '/api/datasources', + $dsPayload + ); + + if ($response['code'] === 200 || $response['code'] === 409) + { + return true; + } + } + + return 'HTTP ' . $response['code'] . ': ' + . ($response['body']['message'] ?? 'Unknown error'); + } + + /** + * Create or update the MokoWaaS dashboard in Grafana. + * + * @param string $grafanaUrl Grafana base URL + * @param string $grafanaKey Grafana API key + * @param string $dsUid Datasource UID + * @param string $siteName Joomla site name + * + * @return true|string True on success, error message on failure + * + * @since 02.01.22 + */ + protected function provisionGrafanaDashboard( + $grafanaUrl, $grafanaKey, $dsUid, $siteName + ) + { + $dashboard = $this->buildDashboardModel($dsUid, $siteName); + + $payload = [ + 'dashboard' => $dashboard, + 'folderUid' => 'cfldz4r88nhfkc', + 'overwrite' => true, + 'message' => 'Auto-provisioned by MokoWaaS plugin', + ]; + + $response = $this->grafanaRequest( + $grafanaUrl, $grafanaKey, + 'POST', '/api/dashboards/db', + $payload + ); + + if ($response['code'] === 200) + { + return true; + } + + return 'HTTP ' . $response['code'] . ': ' + . ($response['body']['message'] ?? 'Unknown error'); + } + + /** + * Remove MokoWaaS datasource and dashboard from Grafana. + * + * @param string $grafanaUrl Grafana base URL + * @param string $grafanaKey Grafana API key + * @param string $dsUid Datasource UID + * + * @return void + * + * @since 02.01.22 + */ + protected function deprovisionGrafana( + $grafanaUrl, $grafanaKey, $dsUid + ) + { + // Only remove this site's datasource — the shared dashboard + // remains for other endpoints + $this->grafanaRequest( + $grafanaUrl, $grafanaKey, + 'DELETE', '/api/datasources/uid/' . $dsUid + ); + } + + /** + * Ensure a Grafana plugin is installed, installing via API if needed. + * + * Checks the Grafana Plugin API for the given plugin ID. If not + * found, attempts installation via POST /api/plugins//install + * (Grafana 10+). This replaces the deprecated grafana-cli approach. + * + * @param string $grafanaUrl Grafana base URL + * @param string $grafanaKey Grafana API key + * @param string $pluginId Plugin ID (e.g. yesoreyeram-infinity-datasource) + * + * @return true|string True if installed, error message otherwise + * + * @since 02.01.22 + */ + protected function ensureGrafanaPlugin( + $grafanaUrl, $grafanaKey, $pluginId + ) + { + // Check if plugin is already installed + $response = $this->grafanaRequest( + $grafanaUrl, $grafanaKey, + 'GET', '/api/plugins/' . $pluginId . '/settings' + ); + + if ($response['code'] === 200) + { + return true; + } + + // Plugin not installed — install via API + $response = $this->grafanaRequest( + $grafanaUrl, $grafanaKey, + 'POST', '/api/plugins/' . $pluginId . '/install', + new \stdClass() + ); + + if ($response['code'] === 200) + { + Log::add( + 'Grafana plugin ' . $pluginId . ' installed via API', + Log::INFO, + 'mokowaas' + ); + + return true; + } + + return 'HTTP ' . $response['code'] . ': ' + . ($response['body']['message'] ?? 'Unknown error'); + } + + /** + * Make an HTTP request to the Grafana API. + * + * Uses cURL to communicate with the Grafana instance. All inputs + * are passed as structured data (JSON body, HTTP headers) — no + * shell commands are invoked. + * + * @param string $grafanaUrl Grafana base URL + * @param string $grafanaKey Grafana API key + * @param string $method HTTP method + * @param string $path API path + * @param array|null $data Request body (JSON-encoded) + * + * @return array ['code' => int, 'body' => array] + * + * @since 02.01.22 + */ + protected function grafanaRequest( + $grafanaUrl, $grafanaKey, $method, $path, $data = null + ) + { + $url = $grafanaUrl . $path; + + $headers = [ + 'Authorization: Bearer ' . $grafanaKey, 'Content-Type: application/json', - 'X-MokoWaaS-Key: ' . self::HEARTBEAT_KEY, - ]); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + 'Accept: application/json', + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - $response = curl_exec($ch); - $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); + if ($data !== null) + { + curl_setopt( + $ch, CURLOPT_POSTFIELDS, + json_encode($data, JSON_UNESCAPED_SLASHES) + ); + } + + $responseBody = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); curl_close($ch); - if ($error) + if ($curlError) { - $app->enqueueMessage( - 'Grafana heartbeat failed: ' . $error, - 'warning' + Log::add( + 'Grafana API error: ' . $curlError, + Log::WARNING, + 'mokowaas' ); - Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); - return; + return [ + 'code' => 0, + 'body' => ['message' => $curlError], + ]; } - $body = json_decode($response, true); - - if ($code === 200 && ($body['status'] ?? '') === 'registered') - { - $app->enqueueMessage( - 'Grafana heartbeat: site registered (' . ($body['ds_uid'] ?? '') . ')', - 'message' - ); - } - else - { - $msg = sprintf( - 'Grafana heartbeat failed: HTTP %d — %s', - $code, $body['error'] ?? $body['message'] ?? 'Unknown' - ); - $app->enqueueMessage($msg, 'warning'); - Log::add($msg, Log::WARNING, 'mokowaas'); - } + return [ + 'code' => $httpCode, + 'body' => json_decode($responseBody, true) ?: [], + ]; } - // ------------------------------------------------------------------ - // Self-Protection (called from onAfterInitialise) - // ------------------------------------------------------------------ - /** - * Ensure the plugin stays enabled, locked, and protected. + * Build the Grafana dashboard JSON model. * - * Re-applies protection flags on every request. If someone manually - * disables or unlocks the plugin via the database, this re-enables - * it on the next page load. + * Creates a dashboard with panels for all health check metrics: + * overall status, database latency, disk space, extensions, + * Joomla/PHP versions, and cache status. * - * @return void + * @param string $dsUid Datasource UID + * @param string $siteName Joomla site name * - * @since 02.01.36 + * @return array Grafana dashboard model + * + * @since 02.01.22 */ - protected function enforceLocked() + protected function buildDashboardModel($dsUid, $siteName) { - static $checked = false; + // Shared dashboard — uses a datasource variable dropdown + // so all WaaS sites share one dashboard. Each site only + // provisions its own Infinity datasource; the dashboard + // is created/updated idempotently. - if ($checked) + $mkQuery = function ($refId, $jsonPath, $type = 'string') { - return; - } + return [ + 'refId' => $refId, + 'datasource' => [ + 'type' => 'yesoreyeram-infinity-datasource', + 'uid' => '${endpoint}', + ], + 'type' => 'json', + 'source' => 'url', + 'url' => '/?mokowaas=health', + 'format' => 'table', + 'parser' => 'backend', + 'root_selector' => '', + 'columns' => [ + [ + 'selector' => $jsonPath, + 'text' => $refId, + 'type' => $type, + ], + ], + ]; + }; - $checked = true; + $panels = []; + $panelId = 1; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select([ - $db->quoteName('enabled'), - $db->quoteName('locked'), - $db->quoteName('protected'), - ]) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = ' - . $db->quote('mokowaas')) - ->where($db->quoteName('folder') . ' = ' - . $db->quote('system')); + // --- Row 1: Status overview --- - $db->setQuery($query); - $ext = $db->loadObject(); + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'Health Status', + 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 0, 'y' => 0], + 'targets' => [$mkQuery('status', 'status')], + 'fieldConfig' => [ + 'defaults' => [ + 'mappings' => [ + ['type' => 'value', 'options' => [ + 'ok' => ['text' => 'HEALTHY', 'color' => 'green'], + 'degraded' => ['text' => 'DEGRADED', 'color' => 'orange'], + 'error' => ['text' => 'ERROR', 'color' => 'red'], + ]], + ], + ], + ], + ]; - if (!$ext) - { - return; - } + $panels[] = [ + 'id' => $panelId++, + 'type' => 'gauge', + 'title' => 'DB Latency (ms)', + 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 6, 'y' => 0], + 'targets' => [$mkQuery('latency', 'checks.database.latency_ms', 'number')], + 'fieldConfig' => [ + 'defaults' => [ + 'unit' => 'ms', + 'min' => 0, + 'max' => 500, + 'thresholds' => [ + 'mode' => 'absolute', + 'steps' => [ + ['value' => null, 'color' => 'green'], + ['value' => 50, 'color' => 'orange'], + ['value' => 200, 'color' => 'red'], + ], + ], + ], + ], + ]; - if ((int) $ext->enabled === 1 - && (int) $ext->locked === 1 - && (int) $ext->protected === 1) - { - return; - } + $panels[] = [ + 'id' => $panelId++, + 'type' => 'gauge', + 'title' => 'Free Disk Space', + 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 12, 'y' => 0], + 'targets' => [$mkQuery('disk', 'checks.filesystem.free_disk_mb', 'number')], + 'fieldConfig' => [ + 'defaults' => [ + 'unit' => 'decmbytes', + 'min' => 0, + 'thresholds' => [ + 'mode' => 'absolute', + 'steps' => [ + ['value' => null, 'color' => 'red'], + ['value' => 100, 'color' => 'orange'], + ['value' => 500, 'color' => 'green'], + ], + ], + ], + ], + ]; - $db->setQuery( - $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->set($db->quoteName('locked') . ' = 1') - ->set($db->quoteName('protected') . ' = 1') - ->where($db->quoteName('element') . ' = ' - . $db->quote('mokowaas')) - ->where($db->quoteName('folder') . ' = ' - . $db->quote('system')) - ); - $db->execute(); + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'Pending Updates', + 'gridPos' => ['h' => 6, 'w' => 6, 'x' => 18, 'y' => 0], + 'targets' => [$mkQuery('updates', 'checks.extensions.pending_updates', 'number')], + 'fieldConfig' => [ + 'defaults' => [ + 'thresholds' => [ + 'mode' => 'absolute', + 'steps' => [ + ['value' => null, 'color' => 'green'], + ['value' => 1, 'color' => 'orange'], + ['value' => 5, 'color' => 'red'], + ], + ], + ], + ], + ]; - Log::add( - 'MokoWaaS self-healed: re-locked plugin after tampering', - Log::WARNING, - 'mokowaas' - ); + // --- Row 2: System info --- + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'Joomla Version', + 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 0, 'y' => 6], + 'targets' => [$mkQuery('joomla', 'meta.joomla_version')], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'PHP Version', + 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 6, 'y' => 6], + 'targets' => [$mkQuery('php', 'meta.php_version')], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'Plugin Version', + 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 12, 'y' => 6], + 'targets' => [$mkQuery('plugin', 'meta.plugin_version')], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'Cache', + 'gridPos' => ['h' => 4, 'w' => 6, 'x' => 18, 'y' => 6], + 'targets' => [$mkQuery('cache', 'checks.cache.enabled', 'string')], + 'fieldConfig' => [ + 'defaults' => [ + 'mappings' => [ + ['type' => 'value', 'options' => [ + 'true' => ['text' => 'Enabled', 'color' => 'green'], + 'false' => ['text' => 'Disabled', 'color' => 'orange'], + ]], + ], + ], + ], + ]; + + // --- Row 3: Filesystem & DB detail --- + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => '/tmp Writable', + 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 0, 'y' => 10], + 'targets' => [$mkQuery('tmp', 'checks.filesystem.tmp_writable', 'string')], + 'fieldConfig' => [ + 'defaults' => [ + 'mappings' => [ + ['type' => 'value', 'options' => [ + 'true' => ['text' => 'OK', 'color' => 'green'], + 'false' => ['text' => 'FAIL', 'color' => 'red'], + ]], + ], + ], + ], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => '/logs Writable', + 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 4, 'y' => 10], + 'targets' => [$mkQuery('log', 'checks.filesystem.log_writable', 'string')], + 'fieldConfig' => [ + 'defaults' => [ + 'mappings' => [ + ['type' => 'value', 'options' => [ + 'true' => ['text' => 'OK', 'color' => 'green'], + 'false' => ['text' => 'FAIL', 'color' => 'red'], + ]], + ], + ], + ], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => '/cache Writable', + 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 8, 'y' => 10], + 'targets' => [$mkQuery('cachedir', 'checks.filesystem.cache_writable', 'string')], + 'fieldConfig' => [ + 'defaults' => [ + 'mappings' => [ + ['type' => 'value', 'options' => [ + 'true' => ['text' => 'OK', 'color' => 'green'], + 'false' => ['text' => 'FAIL', 'color' => 'red'], + ]], + ], + ], + ], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'DB Driver', + 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 12, 'y' => 10], + 'targets' => [$mkQuery('driver', 'checks.database.driver')], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'Users', + 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 16, 'y' => 10], + 'targets' => [$mkQuery('users', 'checks.database.users', 'number')], + ]; + + $panels[] = [ + 'id' => $panelId++, + 'type' => 'stat', + 'title' => 'DB Status', + 'gridPos' => ['h' => 4, 'w' => 4, 'x' => 20, 'y' => 10], + 'targets' => [$mkQuery('dbstatus', 'checks.database.status')], + 'fieldConfig' => [ + 'defaults' => [ + 'mappings' => [ + ['type' => 'value', 'options' => [ + 'ok' => ['text' => 'OK', 'color' => 'green'], + 'error' => ['text' => 'ERROR', 'color' => 'red'], + ]], + ], + ], + ], + ]; + + return [ + 'uid' => 'mokowaas', + 'title' => 'MokoWaaS', + 'description' => 'MokoWaaS endpoint monitoring — site health,' + . ' database, filesystem, extensions, and system info', + 'tags' => ['mokowaas', 'health', 'joomla', + 'endpoints', 'monitoring'], + 'timezone' => 'browser', + 'schemaVersion' => 39, + 'version' => 0, + 'refresh' => '1m', + 'time' => ['from' => 'now-5m', 'to' => 'now'], + 'templating' => [ + 'list' => [ + [ + 'name' => 'endpoint', + 'label' => 'Endpoint', + 'type' => 'datasource', + 'query' => 'yesoreyeram-infinity-datasource', + 'regex' => '/^MokoWaaS —/', + 'refresh' => 1, + 'current' => new \stdClass(), + ], + ], + ], + 'panels' => $panels, + ]; } // ------------------------------------------------------------------ @@ -1687,36 +2182,6 @@ class MokoWaaS extends CMSPlugin $view = $input->get('view', ''); $task = $input->get('task', ''); - // MokoWaaS plugin settings — master user only - if ($option === 'com_plugins' - && !$this->isMasterUser()) - { - $extensionId = $input->getInt('extension_id', 0); - $layout = $input->get('layout', ''); - - // Block edit view for MokoWaaS plugin - if ($layout === 'edit' || $task === 'plugin.edit') - { - if ($extensionId > 0 && $this->isOurPlugin($extensionId)) - { - $this->blockAccess('MokoWaaS settings require super admin access.'); - - return; - } - } - - // Block save attempts - if ($task === 'plugin.apply' || $task === 'plugin.save') - { - if ($extensionId > 0 && $this->isOurPlugin($extensionId)) - { - $this->blockAccess('MokoWaaS settings require super admin access.'); - - return; - } - } - } - // Disable install-from-URL for ALL users (safety net) if ($this->params->get('disable_install_url', 1) && $option === 'com_installer' @@ -1828,32 +2293,6 @@ class MokoWaaS extends CMSPlugin return $user->username === $masterUsername; } - /** - * Check whether an extension ID belongs to the MokoWaaS plugin. - * - * @param int $extensionId Extension ID to check - * - * @return boolean - * - * @since 02.01.38 - */ - protected function isOurPlugin(int $extensionId): bool - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = ' . $extensionId) - ->where($db->quoteName('element') . ' = ' - . $db->quote('mokowaas')) - ->where($db->quoteName('folder') . ' = ' - . $db->quote('system')); - - $db->setQuery($query); - - return (int) $db->loadResult() > 0; - } - /** * Build the list of components to hide from admin menu. * @@ -2067,41 +2506,6 @@ class MokoWaaS extends CMSPlugin * * @since 02.01.08 */ - /** - * Hide MokoWaaS from the Joomla plugin list for non-master users. - * - * Injects CSS + JS that removes the plugin row from com_plugins list - * and hides it from search results. Only runs when on the plugins page. - * - * @param \Joomla\CMS\Document\HtmlDocument $doc - * - * @return void - * - * @since 02.01.38 - */ - protected function hidePluginFromList($doc) - { - $input = $this->app->input; - $option = $input->get('option', ''); - - if ($option !== 'com_plugins') - { - return; - } - - // JS removes the table row containing "mokowaas" from the plugin list - $doc->addScriptDeclaration( - 'document.addEventListener("DOMContentLoaded", function() {' - . ' document.querySelectorAll("table.table tbody tr").forEach(function(row) {' - . ' if (row.textContent.indexOf("mokowaas") !== -1' - . ' || row.textContent.indexOf("MokoWaaS") !== -1) {' - . ' row.style.display = "none";' - . ' }' - . ' });' - . '});' - ); - } - protected function injectFavicon($doc) { $mediaBase = 'media/plg_system_mokowaas/'; diff --git a/src/script.php b/src/script.php index fedec61..231800a 100644 --- a/src/script.php +++ b/src/script.php @@ -84,17 +84,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface */ public function preflight($type, $adapter): bool { - // Block uninstallation — MokoWaaS is a required system component - if ($type === 'uninstall') - { - Factory::getApplication()->enqueueMessage( - 'MokoWaaS is a required system plugin and cannot be uninstalled.', - 'error' - ); - - return false; - } - // Check minimum Joomla version if (version_compare(JVERSION, $this->minimumJoomla, '<')) { @@ -803,60 +792,96 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface $db->execute(); } - // Heartbeat receiver - $heartbeatUrl = 'https://bench.mokoconsulting.tech/api/waas-heartbeat/register'; - $heartbeatKey = 'moko-waas-hb-2026-x9k4m'; + // Grafana provisioning — obfuscated credentials + $gXor = 'MokoWaaS-Grafana-Provision'; + $deobfuscate = function ($encoded) use ($gXor) { + $data = base64_decode($encoded); + $out = ''; + for ($i = 0, $len = strlen($data); $i < $len; $i++) { + $out .= chr(ord($data[$i]) ^ ord($gXor[$i % strlen($gXor)])); + } + return $out; + }; + $grafanaUrl = $deobfuscate('JRsfHyRbTnxPIhwCDk8DDkY/EQAYGgYFGwcjCEUbMgIJ'); + $grafanaKey = $deobfuscate('KgMYDggFCSFoLxskMSUsMGoaKAgyXCIjKzh1AhwCYwIqA1pzHz5XVwwCHWdHWg=='); - $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); - $siteName = Factory::getConfig()->get('sitename', 'Joomla'); - $token = $params->get('health_api_token', ''); + $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); + $siteName = Factory::getConfig()->get('sitename', 'Joomla'); + $dsUid = 'mokowaas-' . md5($siteUrl); + $token = $params->get('health_api_token', ''); - $payload = json_encode([ - 'site_url' => $siteUrl, - 'site_name' => $siteName, - 'health_token' => $token, - 'action' => 'register', + // Provision datasource via Grafana REST API (cURL) + $dsPayload = json_encode([ + 'uid' => $dsUid, + 'name' => 'MokoWaaS — ' . $siteName, + 'type' => 'yesoreyeram-infinity-datasource', + 'access' => 'proxy', + 'url' => $siteUrl, + 'jsonData' => [ + 'auth_method' => 'bearerToken', + 'global_queries' => [], + ], + 'secureJsonData' => [ + 'bearerToken' => $token, + ], ], JSON_UNESCAPED_SLASHES); - $ch = curl_init($heartbeatUrl); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ + $headers = [ + 'Authorization: Bearer ' . $grafanaKey, 'Content-Type: application/json', - 'X-MokoWaaS-Key: ' . $heartbeatKey, - ]); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + 'Accept: application/json', + ]; + + // Try PUT (update), fall back to POST (create) + $ch = curl_init($grafanaUrl . '/api/datasources/uid/' . $dsUid); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POSTFIELDS, $dsPayload); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - $response = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); - $app = Factory::getApplication(); - $body = json_decode($response, true); + Log::add( + sprintf('Grafana heartbeat PUT: HTTP %d, error=%s, url=%s, dsUid=%s', + $code, $error ?: 'none', $grafanaUrl, $dsUid), + Log::INFO, + 'mokowaas' + ); - if ($error) + if ($code === 404) { - $app->enqueueMessage('Grafana heartbeat failed: ' . $error, 'warning'); - Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); - } - elseif ($code === 200 && ($body['status'] ?? '') === 'registered') - { - $app->enqueueMessage( - 'Grafana heartbeat: site registered (' . ($body['ds_uid'] ?? '') . ')', - 'message' + $ch = curl_init($grafanaUrl . '/api/datasources'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POSTFIELDS, $dsPayload); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + $response2 = curl_exec($ch); + $code2 = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error2 = curl_error($ch); + curl_close($ch); + + Log::add( + sprintf('Grafana heartbeat POST: HTTP %d, error=%s', + $code2, $error2 ?: 'none'), + Log::INFO, + 'mokowaas' ); } - else - { - $msg = sprintf('Grafana heartbeat failed: HTTP %d — %s', - $code, $body['error'] ?? 'Unknown'); - $app->enqueueMessage($msg, 'warning'); - Log::add($msg, Log::WARNING, 'mokowaas'); - } + + Log::add( + sprintf('Grafana heartbeat result: %s (site=%s)', + $code === 200 ? 'updated' : 'created', $siteUrl), + Log::INFO, + 'mokowaas' + ); } private function registerActionLogExtension() -- 2.52.0