feat: Send Heartbeat button next to token field
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled

Adds a heart icon button in the token field input group that triggers
an AJAX heartbeat to MokoWaaSBase. Shows spinner while sending,
green check on success, red X on failure. Uses the monitor plugin's
configured base_url and the core plugin's health_api_token.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-06 17:40:09 -05:00
parent 2ff323b920
commit de78e66da1
2 changed files with 119 additions and 2 deletions
@@ -80,6 +80,96 @@ class DisplayController extends BaseController
}
// ==================================================================
// Heartbeat
// ==================================================================
public function sendHeartbeat()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
try
{
$monitorPlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokowaas_monitor');
if (!$monitorPlugin)
{
$this->jsonResponse(['success' => false, 'message' => 'Monitor plugin not enabled.']);
return;
}
$params = new \Joomla\Registry\Registry($monitorPlugin->params);
$baseUrl = rtrim($params->get('base_url', ''), '/');
if (empty($baseUrl))
{
$this->jsonResponse(['success' => false, 'message' => 'MokoWaaSBase URL not configured in monitor plugin.']);
return;
}
$corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokowaas');
$coreParams = new \Joomla\Registry\Registry($corePlugin ? $corePlugin->params : '{}');
$healthToken = $coreParams->get('health_api_token', '');
if (empty($healthToken))
{
$this->jsonResponse(['success' => false, 'message' => 'Health token not configured.']);
return;
}
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
$payload = json_encode([
'token' => $healthToken,
'domain' => $domain,
'site_name' => Factory::getConfig()->get('sitename', 'Joomla'),
'site_url' => $siteUrl,
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
'php_version' => PHP_VERSION,
], JSON_UNESCAPED_SLASHES);
$endpoint = $baseUrl . '/api/index.php/v1/mokowaasbase/heartbeat';
$ch = curl_init($endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
]);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error)
{
$this->jsonResponse(['success' => false, 'message' => 'Connection failed: ' . $error]);
}
elseif ($code >= 200 && $code < 300)
{
$body = json_decode($response, true);
$this->jsonResponse(['success' => true, 'message' => 'Heartbeat sent: ' . ($body['status'] ?? 'ok')]);
}
else
{
$body = json_decode($response, true);
$this->jsonResponse(['success' => false, 'message' => 'HTTP ' . $code . ': ' . ($body['error'] ?? $body['message'] ?? 'Unknown')]);
}
}
catch (\Throwable $e)
{
$this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
}
}
// Cache
// ==================================================================
@@ -18,6 +18,7 @@ namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Session\Session;
/**
* Renders a read-only text input with a "Copy" button, similar to
@@ -39,8 +40,9 @@ class CopyableTokenField extends FormField
return '<div class="alert alert-warning mb-0 py-2">Token will be generated automatically on first save.</div>';
}
// Derive a human-readable support PIN from the token
$pin = strtoupper(substr($this->value, 0, 4) . '-' . substr($this->value, 4, 4));
$pin = strtoupper(substr($this->value, 0, 4) . '-' . substr($this->value, 4, 4));
$token = Session::getFormToken();
$ajaxUrl = 'index.php?option=com_mokowaas&task=display.sendHeartbeat&format=json';
return <<<HTML
<div class="input-group mb-2">
@@ -60,6 +62,31 @@ class CopyableTokenField extends FormField
inp.select(); document.execCommand('copy');
}
"><span class="icon-copy" aria-hidden="true"></span> Copy</button>
<button type="button" class="btn btn-outline-primary" id="mokowaas-send-heartbeat" onclick="
var btn = this;
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = '<span class=&quot;icon-spinner icon-spin&quot; aria-hidden=&quot;true&quot;></span> Sending...';
var fd = new FormData();
fd.append('{$token}', '1');
fetch('{$ajaxUrl}', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if(d.success){
btn.innerHTML='<span class=&quot;icon-check&quot; aria-hidden=&quot;true&quot;></span> Sent';
btn.classList.replace('btn-outline-primary','btn-success');
} else {
btn.innerHTML='<span class=&quot;icon-times&quot; aria-hidden=&quot;true&quot;></span> Failed';
btn.classList.replace('btn-outline-primary','btn-danger');
}
setTimeout(function(){btn.innerHTML=orig;btn.className='btn btn-outline-primary';btn.disabled=false;},3000);
})
.catch(function(){
btn.innerHTML='<span class=&quot;icon-times&quot; aria-hidden=&quot;true&quot;></span> Error';
btn.classList.replace('btn-outline-primary','btn-danger');
setTimeout(function(){btn.innerHTML=orig;btn.className='btn btn-outline-primary';btn.disabled=false;},3000);
});
"><span class="icon-heart" aria-hidden="true"></span> Send Heartbeat</button>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-dark" style="font-family:monospace;font-size:1rem;letter-spacing:0.1em;">MOKO-{$pin}</span>