diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01b35f20..fe399ee7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,12 @@
- **Frontend link in status bar** — cache/temp module now has 4 buttons: Site (frontend link), PIN, Cache, Temp
- **Help buttons** — all admin views link to Gitea wiki pages via toolbar help button
- **Support PIN in heartbeat** — core system plugin includes current PIN in heartbeat payload to HQ
+- **HQ config sync** — client stores HQ-configured `support_pin_hours` from heartbeat response, PIN TTL now configurable from HQ
+
+### Changed
+- Admin sidebar menu module now loads component-local language files (fixes untranslated keys for MokoSuiteCross and other components)
+- Support PIN TTL is now configurable via HQ global options instead of hardcoded 72 hours
+- Removed MokoSuiteHQ from extension catalog (internal app, not for client sites)
- **SupportPinHelper** — shared helper centralises PIN generation across dashboard, cpanel module, cache module, and AJAX controller
- **Current IP display** — firewall plugin settings show admin's IP with copy button
- **Heartbeat monitor** — consolidated into core plugin from retired monitor plugin, with diagnostic logging on all bail-out points
diff --git a/source/packages/com_mokosuiteclient/admin/catalog.xml b/source/packages/com_mokosuiteclient/admin/catalog.xml
index b1598f85..266125d1 100644
--- a/source/packages/com_mokosuiteclient/admin/catalog.xml
+++ b/source/packages/com_mokosuiteclient/admin/catalog.xml
@@ -20,15 +20,6 @@
true
https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/updates.xml
-
- MokoSuiteHQ
- pkg_mokosuitehq
- package
- Centralized control panel for managing all MokoSuite client installations.
- icon-tachometer-alt
- Platform
- https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHQ/raw/branch/main/updates.xml
-
MokoSuiteBackup
pkg_mokosuitebackup
diff --git a/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php b/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php
index 8571c1fd..3d1569e3 100644
--- a/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php
+++ b/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php
@@ -23,8 +23,8 @@ use Joomla\Database\DatabaseInterface;
*/
class SupportPinHelper
{
- /** @var int PIN validity window in seconds (72 hours) */
- public const PIN_TTL = 72 * 3600;
+ /** @var int Default PIN validity window in seconds (72 hours) */
+ public const PIN_TTL_DEFAULT = 72 * 3600;
/**
* Load core plugin params and return PIN state.
@@ -74,11 +74,12 @@ class SupportPinHelper
$result['available'] = true;
+ $pinTtl = (int) ($params['support_pin_hours'] ?? 0) * 3600 ?: self::PIN_TTL_DEFAULT;
$requestedAt = (int) ($params['support_pin_requested_at'] ?? 0);
- if ($requestedAt && (time() - $requestedAt) < self::PIN_TTL)
+ if ($requestedAt && (time() - $requestedAt) < $pinTtl)
{
- $result['pin'] = self::generate($token, $requestedAt);
+ $result['pin'] = self::generate($token, $requestedAt, $pinTtl);
}
}
catch (\Throwable $e)
@@ -94,12 +95,14 @@ class SupportPinHelper
*
* @param string $token Health API token (HMAC key).
* @param int $timestamp The request timestamp.
+ * @param int $ttl PIN validity window in seconds.
*
* @return string e.g. "MOKO-A1B2-C3D4"
*/
- public static function generate(string $token, int $timestamp): string
+ public static function generate(string $token, int $timestamp, int $ttl = 0): string
{
- $window = floor($timestamp / self::PIN_TTL);
+ $ttl = $ttl ?: self::PIN_TTL_DEFAULT;
+ $window = floor($timestamp / $ttl);
$hash = hash_hmac('sha256', (string) $window, $token);
return 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
@@ -121,7 +124,7 @@ class SupportPinHelper
return ['success' => false, 'message' => 'Health token not configured.'];
}
- $now = time();
+ $now = time();
$params = $state['params'];
$params['support_pin_requested_at'] = $now;
@@ -132,8 +135,10 @@ class SupportPinHelper
$db->setQuery($query)->execute();
- $pin = self::generate($state['token'], $now);
+ $pinHours = (int) ($params['support_pin_hours'] ?? 0) ?: (int) (self::PIN_TTL_DEFAULT / 3600);
+ $pinTtl = $pinHours * 3600;
+ $pin = self::generate($state['token'], $now, $pinTtl);
- return ['success' => true, 'pin' => $pin, 'message' => 'PIN generated — valid for 72 hours.'];
+ return ['success' => true, 'pin' => $pin, 'message' => 'PIN generated — valid for ' . $pinHours . ' hours.'];
}
}
diff --git a/source/packages/mod_mokosuiteclient_menu/tmpl/default.php b/source/packages/mod_mokosuiteclient_menu/tmpl/default.php
index 26d0157a..b6fef286 100644
--- a/source/packages/mod_mokosuiteclient_menu/tmpl/default.php
+++ b/source/packages/mod_mokosuiteclient_menu/tmpl/default.php
@@ -65,6 +65,15 @@ try
{
$lang->load($m->element . '.sys', JPATH_ADMINISTRATOR);
$lang->load($m->element, JPATH_ADMINISTRATOR);
+
+ // Also try component-local language path (Joomla 5/6 pattern)
+ $compLangPath = JPATH_ADMINISTRATOR . '/components/' . $m->element;
+ if (is_dir($compLangPath . '/language'))
+ {
+ $lang->load($m->element . '.sys', $compLangPath);
+ $lang->load($m->element, $compLangPath);
+ }
+
$loadedLangs[$m->element] = true;
}
}
diff --git a/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php b/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php
index 93f5e0ba..4eb75cbf 100644
--- a/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php
+++ b/source/packages/plg_system_mokosuiteclient/Extension/MokoSuiteClient.php
@@ -2743,6 +2743,13 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
if ($response->code >= 200 && $response->code < 300)
{
+ $body = json_decode($response->body, true);
+
+ if (!empty($body['config']['support_pin_hours']))
+ {
+ $this->syncHqConfig('support_pin_hours', (int) $body['config']['support_pin_hours']);
+ }
+
$this->app->enqueueMessage('MokoSuiteHQ heartbeat: site registered successfully.', 'message');
}
else
@@ -2763,6 +2770,47 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
}
}
+ private function syncHqConfig(string $key, $value): void
+ {
+ try
+ {
+ $db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
+ $query = $db->getQuery(true)
+ ->select($db->quoteName(['extension_id', 'params']))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
+
+ $ext = $db->setQuery($query)->loadObject();
+
+ if (!$ext)
+ {
+ return;
+ }
+
+ $params = json_decode($ext->params, true) ?: [];
+
+ if (($params[$key] ?? null) === $value)
+ {
+ return;
+ }
+
+ $params[$key] = $value;
+
+ $db->setQuery(
+ $db->getQuery(true)
+ ->update($db->quoteName('#__extensions'))
+ ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
+ ->where($db->quoteName('extension_id') . ' = ' . (int) $ext->extension_id)
+ )->execute();
+ }
+ catch (\Throwable $e)
+ {
+ // Config sync is non-critical
+ }
+ }
+
private function signHeartbeatRequest(string $domain, int $timestamp, string $token): ?string
{
$signingKeyB64 = $this->params->get('monitor_signing_key', '');