From b22842f302c53bccc770869d137f77f06775e05d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 22 May 2026 04:40:37 -0500 Subject: [PATCH] refactor: replace Grafana API with heartbeat receiver provisioning Remove all Grafana API code (630 lines), obfuscated tokens, SA tokens, ensureGrafanaPlugin, provisionGrafanaDatasource, buildDashboardModel. Replace with simple HTTP POST to heartbeat receiver on bench server. Receiver writes Grafana provisioning YAML and restarts Grafana container. No API tokens or RBAC permissions needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Extension/MokoWaaS.php | 680 +++---------------------------------- src/script.php | 142 ++------ 2 files changed, 81 insertions(+), 741 deletions(-) diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index 473fbafe..ec25880a 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -50,51 +50,20 @@ use Joomla\CMS\User\UserHelper; class MokoWaaS extends CMSPlugin { /** - * Obfuscated Grafana URL (XOR + base64). + * Heartbeat receiver URL for Grafana provisioning. * * @var string - * @since 02.01.26 + * @since 02.01.36 */ - private const G_URL = 'JRsfHyRbTnxPIhwCDk8DDkY/EQAYGgYFGwcjCEUbMgIJ'; + private const HEARTBEAT_URL = 'https://bench.mokoconsulting.tech/api/waas-heartbeat'; /** - * Obfuscated Grafana service account token (XOR + base64). + * Shared secret for heartbeat authentication. * * @var string - * @since 02.01.26 + * @since 02.01.36 */ - private const G_KEY = 'KgMYDggTBwJbcRMsD1MDMFw8OQUvLDA5Xho6ACUpYwBREkYxPT5RBAxXTGRBVg=='; - - /** - * 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; - } + private const HEARTBEAT_KEY = 'moko-waas-hb-2026-x9k4m'; /** * Load the language file on instantiation. @@ -1341,636 +1310,89 @@ class MokoWaaS extends CMSPlugin $this->app->close(); } + // ------------------------------------------------------------------ - // Grafana Provisioning (called from onExtensionAfterSave) + // Heartbeat (called from onExtensionAfterSave) // ------------------------------------------------------------------ /** - * Handle Grafana datasource and dashboard provisioning. + * Send heartbeat to the MokoWaaS monitoring receiver. * - * 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. + * Registers this site with the Grafana provisioning system. + * The receiver writes a datasource YAML file and restarts Grafana. * * @param \Joomla\Registry\Registry $params Plugin params * @param \Joomla\CMS\Application\CMSApplication $app Application * * @return void * - * @since 02.01.22 + * @since 02.01.36 */ 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; } - // Ensure Infinity datasource plugin is installed - $pluginOk = $this->ensureGrafanaPlugin( - $grafanaUrl, $grafanaKey, - 'yesoreyeram-infinity-datasource' - ); + $siteUrl = rtrim(Uri::root(), '/'); + $siteName = Factory::getConfig()->get('sitename', 'Joomla'); - if ($pluginOk !== true) - { - $app->enqueueMessage( - 'Grafana plugin install failed: ' . $pluginOk - . ' — install the Infinity plugin manually.', - 'warning' - ); + $payload = json_encode([ + 'site_url' => $siteUrl, + 'site_name' => $siteName, + 'health_token' => $healthToken, + 'action' => 'register', + ], JSON_UNESCAPED_SLASHES); - 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, + $ch = curl_init(self::HEARTBEAT_URL . '/register'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', - 'Accept: application/json', - ]; - - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + 'X-MokoWaaS-Key: ' . self::HEARTBEAT_KEY, + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 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); - 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); + $response = curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); curl_close($ch); - if ($curlError) + if ($error) { - Log::add( - 'Grafana API error: ' . $curlError, - Log::WARNING, - 'mokowaas' + $app->enqueueMessage( + 'Grafana heartbeat failed: ' . $error, + 'warning' ); + Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); - return [ - 'code' => 0, - 'body' => ['message' => $curlError], - ]; + return; } - return [ - 'code' => $httpCode, - 'body' => json_decode($responseBody, true) ?: [], - ]; - } + $body = json_decode($response, true); - /** - * Build the Grafana dashboard JSON model. - * - * Creates a dashboard with panels for all health check metrics: - * overall status, database latency, disk space, extensions, - * Joomla/PHP versions, and cache status. - * - * @param string $dsUid Datasource UID - * @param string $siteName Joomla site name - * - * @return array Grafana dashboard model - * - * @since 02.01.22 - */ - protected function buildDashboardModel($dsUid, $siteName) - { - // 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. - - $mkQuery = function ($refId, $jsonPath, $type = 'string') + if ($code === 200 && ($body['status'] ?? '') === 'registered') { - 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, - ], - ], - ]; - }; - - $panels = []; - $panelId = 1; - - // --- Row 1: Status overview --- - - $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'], - ]], - ], - ], - ], - ]; - - $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'], - ], - ], - ], - ], - ]; - - $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'], - ], - ], - ], - ], - ]; - - $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'], - ], - ], - ], - ], - ]; - - // --- 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, - ]; + $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'); + } } // ------------------------------------------------------------------ diff --git a/src/script.php b/src/script.php index 33d40d97..6be105bd 100644 --- a/src/script.php +++ b/src/script.php @@ -792,142 +792,60 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface $db->execute(); } - // 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('KgMYDggTBwJbcRMsD1MDMFw8OQUvLDA5Xho6ACUpYwBREkYxPT5RBAxXTGRBVg=='); + // Heartbeat receiver + $heartbeatUrl = 'https://bench.mokoconsulting.tech/api/waas-heartbeat/register'; + $heartbeatKey = 'moko-waas-hb-2026-x9k4m'; - $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); - $siteName = Factory::getConfig()->get('sitename', 'Joomla'); - $dsUid = 'mokowaas-' . md5($siteUrl); - $token = $params->get('health_api_token', ''); + $siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/'); + $siteName = Factory::getConfig()->get('sitename', 'Joomla'); + $token = $params->get('health_api_token', ''); - // 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, - ], + $payload = json_encode([ + 'site_url' => $siteUrl, + 'site_name' => $siteName, + 'health_token' => $token, + 'action' => 'register', ], JSON_UNESCAPED_SLASHES); - $headers = [ - 'Authorization: Bearer ' . $grafanaKey, + $ch = curl_init($heartbeatUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', - '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); + 'X-MokoWaaS-Key: ' . $heartbeatKey, + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); 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(); + $app = Factory::getApplication(); + $body = json_decode($response, true); if ($error) { - $msg = 'Grafana heartbeat failed: ' . $error; - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); - - return; + $app->enqueueMessage('Grafana heartbeat failed: ' . $error, 'warning'); + Log::add('Heartbeat failed: ' . $error, Log::WARNING, 'mokowaas'); } - - Log::add( - sprintf('Grafana heartbeat PUT: HTTP %d, url=%s, dsUid=%s', - $code, $grafanaUrl, $dsUid), - Log::INFO, - 'mokowaas' - ); - - if ($code === 200) + elseif ($code === 200 && ($body['status'] ?? '') === 'registered') { $app->enqueueMessage( - 'Grafana heartbeat: datasource updated.', + 'Grafana heartbeat: site registered (' . ($body['ds_uid'] ?? '') . ')', 'message' ); - - return; } - - if ($code === 404) + else { - // Datasource doesn't exist — create it - $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); - - if ($error2) - { - $msg = 'Grafana heartbeat create failed: ' . $error2; - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); - - return; - } - - if ($code2 === 200 || $code2 === 409) - { - $app->enqueueMessage( - 'Grafana heartbeat: datasource registered.', - 'message' - ); - } - else - { - $body2 = json_decode($response2, true); - $msg = sprintf( - 'Grafana heartbeat failed: HTTP %d — %s', - $code2, $body2['message'] ?? 'Unknown error' - ); - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); - } - - return; + $msg = sprintf('Grafana heartbeat failed: HTTP %d — %s', + $code, $body['error'] ?? 'Unknown'); + $app->enqueueMessage($msg, 'warning'); + Log::add($msg, Log::WARNING, 'mokowaas'); } - - // Any other HTTP code (403, 500, etc.) - $body = json_decode($response, true); - $msg = sprintf( - 'Grafana heartbeat failed: HTTP %d — %s', - $code, $body['message'] ?? 'Unknown error' - ); - Log::add($msg, Log::WARNING, 'mokowaas'); - $app->enqueueMessage($msg, 'warning'); } private function registerActionLogExtension()