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()