Files
MokoSuiteClient/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php
T
Jonathan Miller 8fa87ef1d7
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 50s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
Generic: Project CI / Tests (pull_request) Has been cancelled
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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
style: label all info bar badges with title + version in both views
MokoSuite 02.47.xx | Joomla 6.1.1 | PHP 8.4.20 | mysql
2026-06-23 11:41:02 -05:00

448 lines
19 KiB
PHP

<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
/** @var \Moko\Component\MokoSuiteClient\Administrator\View\Dashboard\HtmlView $this */
$siteInfo = $this->siteInfo;
$plugins = $this->plugins;
$recentLogins = $this->recentLogins;
$pendingUpdates = $this->pendingUpdates;
$mokoExts = $this->mokoExtensions;
$adminToolsAvail = $this->adminToolsAvailable ?? null;
$atsAvail = $this->atsAvailable ?? null;
$checkedOut = $this->checkedOutItems;
$wafBlocks = $this->wafBlocks;
$token = Session::getFormToken();
// Group plugins by category
$grouped = [];
foreach ($plugins as $plugin)
{
$grouped[$plugin->category][] = $plugin;
}
$categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
$actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_actionlogs');
?>
<div id="mokosuiteclient-dashboard">
<?php if (!$actionLogsEnabled): ?>
<div class="alert alert-danger d-flex align-items-center gap-2 mb-4">
<span class="icon-exclamation-triangle" style="font-size:1.25rem"></span>
<div>
<strong>Action Logs Required</strong> — MokoSuite requires Joomla's Action Logs component to be enabled for login tracking and audit compliance.
<a href="<?php echo Route::_('index.php?option=com_plugins&filter[search]=actionlog'); ?>" class="alert-link ms-1">Enable Action Log Plugins</a>
</div>
</div>
<?php endif; ?>
<!-- Site Info Bar -->
<div class="card mb-4">
<div class="card-body d-flex flex-wrap align-items-center gap-2" style="padding:0.75rem 1.25rem;font-size:0.85rem;">
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem;color:#1a2744"></span>
<span class="fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
<span class="badge bg-primary">MokoSuite <?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span>
<?php if (!empty($this->supportPin)): ?>
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span>
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-1" id="mokosuiteclient-btn-heartbeat-pin"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.sendHeartbeat&format=json'); ?>"
data-token="<?php echo $token; ?>"
title="Send heartbeat with PIN to MokoSuiteHQ">
<span class="icon-upload" aria-hidden="true"></span>
</button>
<?php endif; ?>
<span class="badge bg-secondary">Joomla <?php echo $this->escape($siteInfo->joomla_version); ?></span>
<span class="badge bg-secondary">PHP <?php echo $this->escape($siteInfo->php_version); ?></span>
<span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span>
<?php if ($siteInfo->debug): ?>
<span class="badge bg-warning text-dark">Debug ON</span>
<?php endif; ?>
<?php if ($siteInfo->offline): ?>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
<span class="ms-auto d-flex align-items-center gap-2">
<span class="icon-globe" aria-hidden="true"></span>
<code><?php echo $this->escape($_SERVER['REMOTE_ADDR'] ?? ''); ?></code>
</span>
</div>
</div>
<?php if ($adminToolsAvail || $atsAvail): ?>
<!-- Akeeba Import Banner -->
<div class="alert alert-info d-flex flex-wrap align-items-center gap-3 mb-4">
<span class="icon-info-circle" style="font-size:1.25rem"></span>
<strong>Akeeba data detected — import into MokoSuiteClient:</strong>
<?php if ($adminToolsAvail): ?>
<button type="button" class="btn btn-sm btn-info" id="btn-import-admintools"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.importAdminTools&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-shield-alt"></span> Import Admin Tools Settings
</button>
<?php endif; ?>
<?php if ($atsAvail): ?>
<button type="button" class="btn btn-sm btn-info" id="btn-import-ats-dash"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.importAts&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-headphones"></span> Import Tickets (<?php echo $atsAvail->tickets; ?> tickets)
</button>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Quick Actions (large buttons) -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-4 col-xl-3">
<button type="button" class="btn btn-outline-primary w-100 py-3" id="mokosuiteclient-btn-cache"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.clearCache&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-bolt d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Clear Cache
</button>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="btn btn-outline-primary w-100 py-3">
<span class="icon-refresh d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Check Updates
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=extensions'); ?>" class="btn btn-outline-primary w-100 py-3">
<span class="icon-puzzle-piece d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Moko Extensions
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-check-square d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Global Check-in
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_actionlogs'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-list d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
View Logs
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_scheduler'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-clock d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Scheduled Tasks
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<?php
// Use MokoJoomCommunity if available, otherwise Joomla user manager
$useCB = file_exists(JPATH_ADMINISTRATOR . '/components/com_comprofiler/comprofiler.php');
$userUrl = $useCB
? Route::_('index.php?option=com_comprofiler&task=showusers')
: Route::_('index.php?option=com_users');
$userLabel = $useCB ? 'MokoJoomCommunity' : 'User Manager';
?>
<a href="<?php echo $userUrl; ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-users d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo $userLabel; ?>
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_redirect'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-arrow-right d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Redirects
</a>
</div>
</div>
<!-- Three-column layout: plugins left, tables right -->
<div class="row">
<!-- Left: Feature Plugin Grid (8 cols) -->
<div class="col-12 col-xl-8">
<?php foreach ($categoryOrder as $catKey): ?>
<?php if (empty($grouped[$catKey])) continue; ?>
<?php
$catPlugins = $grouped[$catKey];
$first = $catPlugins[0];
?>
<h3 class="mokosuiteclient-category-heading mb-3">
<span class="badge <?php echo $this->escape($first->categoryBadge); ?>"><?php echo $this->escape($first->categoryLabel); ?></span>
</h3>
<div class="mokosuiteclient-plugin-grid row g-3 mb-4">
<?php foreach ($catPlugins as $plugin): ?>
<div class="col-12 <?php echo $catKey === 'core' ? '' : 'col-md-6 col-lg-4'; ?>">
<div class="card mokosuiteclient-plugin-card h-100 <?php echo $plugin->enabled ? '' : 'mokosuiteclient-plugin-disabled'; ?>"
data-extension-id="<?php echo $plugin->extension_id; ?>">
<div class="card-body d-flex flex-column">
<div class="d-flex align-items-start justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<span class="<?php echo $this->escape($plugin->icon); ?> mokosuiteclient-plugin-icon" aria-hidden="true"></span>
<h5 class="card-title mb-0"><?php echo $this->escape($plugin->name); ?></h5>
</div>
<?php if ($plugin->version): ?>
<span class="badge bg-light text-dark"><?php echo $this->escape($plugin->version); ?></span>
<?php endif; ?>
</div>
<p class="card-text text-muted text-muted flex-grow-1"><?php echo $this->escape($plugin->description); ?></p>
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
<?php if ($plugin->protected): ?>
<span class="badge bg-dark"><?php echo Text::_('COM_MOKOSUITECLIENT_PROTECTED'); ?></span>
<?php elseif ($plugin->configure_only): ?>
<span class="badge bg-<?php echo $plugin->enabled ? 'success' : 'secondary'; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOSUITECLIENT_ENABLED') : Text::_('COM_MOKOSUITECLIENT_DISABLED'); ?>
</span>
<?php else: ?>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input mokosuiteclient-toggle" role="switch"
id="toggle-<?php echo $plugin->extension_id; ?>"
data-extension-id="<?php echo $plugin->extension_id; ?>"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.togglePlugin&format=json'); ?>"
data-token="<?php echo $token; ?>"
<?php echo $plugin->enabled ? 'checked' : ''; ?>>
<label class="form-check-label" for="toggle-<?php echo $plugin->extension_id; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOSUITECLIENT_ENABLED') : Text::_('COM_MOKOSUITECLIENT_DISABLED'); ?>
</label>
</div>
<?php endif; ?>
<?php if ($plugin->type === 'plugin'): ?>
<a href="<?php echo Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $plugin->extension_id); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-cog" aria-hidden="true"></span> <?php echo Text::_('COM_MOKOSUITECLIENT_CONFIGURE'); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Right: Charts & Information (4 cols) -->
<div class="col-12 col-xl-4" style="border-left:1px solid var(--gray-300, #dee2e6);padding-left:1.5rem;">
<!-- WAF Activity Chart -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-shield-alt" aria-hidden="true"></span> WAF Activity (14 days)</strong>
</div>
<div class="card-body py-2">
<canvas id="mokosuiteclient-chart-waf" height="140"></canvas>
</div>
</div>
<!-- Login Activity Chart -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-user" aria-hidden="true"></span> Login Activity (14 days)</strong>
</div>
<div class="card-body py-2">
<canvas id="mokosuiteclient-chart-logins" height="140"></canvas>
</div>
</div>
<!-- Pending Updates -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-refresh" aria-hidden="true"></span> Pending Updates</strong>
<span class="badge bg-<?php echo count($pendingUpdates) > 0 ? 'warning text-dark' : 'success'; ?>"><?php echo count($pendingUpdates); ?></span>
</div>
<?php if (!empty($pendingUpdates)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>Extension</th><th>Current</th><th>Available</th></tr></thead>
<tbody>
<?php foreach ($pendingUpdates as $upd): ?>
<tr>
<td class="text-muted"><?php echo $this->escape($upd->name); ?></td>
<td class="text-muted"><?php echo $this->escape($upd->current_version); ?></td>
<td class="text-success fw-bold"><?php echo $this->escape($upd->version); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> All extensions up to date
</div>
<?php endif; ?>
</div>
<!-- Checked Out Items -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-lock" aria-hidden="true"></span> Checked Out Items</strong>
<span class="badge bg-<?php echo count($checkedOut) > 0 ? 'info' : 'success'; ?>"><?php echo count($checkedOut); ?></span>
</div>
<?php if (!empty($checkedOut)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>Article</th><th>User</th><th>Since</th></tr></thead>
<tbody>
<?php foreach ($checkedOut as $item): ?>
<tr>
<td class="text-muted"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td>
<td class="text-muted"><?php echo $this->escape($item->username ?? ''); ?></td>
<td class="text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, Text::_('DATE_FORMAT_LC4')); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card-footer text-center py-1">
<a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="text-muted">Global Check-in</a>
</div>
<?php else: ?>
<div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> No checked out items
</div>
<?php endif; ?>
</div>
<!-- WAF Blocks -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-shield-alt" aria-hidden="true"></span> Recent WAF Blocks</strong>
<span class="badge bg-<?php echo count($wafBlocks) > 0 ? 'danger' : 'success'; ?>"><?php echo count($wafBlocks); ?></span>
</div>
<?php if (!empty($wafBlocks)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>IP</th><th>Rule</th><th>Time</th></tr></thead>
<tbody>
<?php foreach ($wafBlocks as $block): ?>
<tr>
<td class="text-muted"><code><?php echo $this->escape($block->ip); ?></code></td>
<td class="text-muted"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td>
<td class="text-muted"><?php echo HTMLHelper::_('date', $block->created, Text::_('DATE_FORMAT_LC4')); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> No recent blocks
</div>
<?php endif; ?>
</div>
<!-- Recent Logins -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-user" aria-hidden="true"></span> Recent Logins</strong>
</div>
<?php if (!empty($recentLogins)): ?>
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead><tr><th>User</th><th>App</th><th>IP</th><th>Time</th></tr></thead>
<tbody>
<?php foreach ($recentLogins as $login):
$msgData = json_decode($login->message ?? '{}');
$appKey = $msgData->app ?? '';
if (stripos($appKey, 'ADMINISTRATOR') !== false) {
$appLabel = 'Admin';
$appBadge = 'bg-dark';
} elseif (stripos($appKey, 'SITE') !== false) {
$appLabel = 'Site';
$appBadge = 'bg-info text-dark';
} else {
$appLabel = 'Unknown';
$appBadge = 'bg-secondary';
}
?>
<tr>
<td class="text-muted"><?php echo $this->escape($login->username ?? ''); ?></td>
<td><span class="badge <?php echo $appBadge; ?>" style="font-size:0.7rem;"><?php echo $appLabel; ?></span></td>
<?php $ip = $login->ip_address ?? ''; ?>
<td class="text-muted"><?php if ($ip && $ip !== 'COM_ACTIONLOGS_DISABLED'): ?><code><?php echo $this->escape($ip); ?></code><?php else: ?><span class="text-muted fst-italic">IP logging off</span><?php endif; ?></td>
<td class="text-muted"><?php echo HTMLHelper::_('date', $login->log_date, Text::_('DATE_FORMAT_LC4')); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted py-3">No login activity recorded</div>
<?php endif; ?>
</div>
</div><!-- /.col-xl-4 -->
</div><!-- /.row -->
</div>
<?php
// Prepare chart data as JSON for JavaScript
$wafChartData = $this->wafChartData ?? [];
$loginChartData = $this->loginChartData ?? [];
$wafLabels = array_map(fn($d) => $d->day, $wafChartData);
$wafValues = array_map(fn($d) => $d->total, $wafChartData);
$loginLabels = array_map(fn($d) => $d->day, $loginChartData);
$loginValues = array_map(fn($d) => $d->total, $loginChartData);
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var chartDefaults = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { maxRotation: 45, font: { size: 10 } } },
y: { beginAtZero: true, ticks: { stepSize: 1, font: { size: 10 } } }
}
};
// WAF chart
var wafCtx = document.getElementById('mokosuiteclient-chart-waf');
if (wafCtx) {
new Chart(wafCtx, {
type: 'bar',
data: {
labels: <?php echo json_encode($wafLabels); ?>,
datasets: [{
data: <?php echo json_encode($wafValues); ?>,
backgroundColor: 'rgba(197, 40, 39, 0.6)',
borderColor: '#c52827',
borderWidth: 1,
borderRadius: 3
}]
},
options: chartDefaults
});
}
// Login chart
var loginCtx = document.getElementById('mokosuiteclient-chart-logins');
if (loginCtx) {
new Chart(loginCtx, {
type: 'line',
data: {
labels: <?php echo json_encode($loginLabels); ?>,
datasets: [{
data: <?php echo json_encode($loginValues); ?>,
borderColor: '#2a69b8',
backgroundColor: 'rgba(42, 105, 184, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
pointBackgroundColor: '#2a69b8'
}]
},
options: chartDefaults
});
}
});
</script>