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
MokoSuite 02.47.xx | Joomla 6.1.1 | PHP 8.4.20 | mysql
448 lines
19 KiB
PHP
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>
|