Merge pull request 'feat: Dashboard snapshot widget, backup trend, storage breakdown (#61)' (#93) from feat/61-dashboard-widgets into main
This commit was merged in pull request #93.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Dashboard: snapshot widget, backup trend chart (30 days), and storage breakdown by profile (#61)
|
||||
|
||||
## [01.33.00] --- 2026-06-23
|
||||
|
||||
## [01.33.00] --- 2026-06-23
|
||||
|
||||
@@ -33,6 +33,12 @@ COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SNAPSHOTS="Content Snapshots"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_VIEW_ALL="View All"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_LATEST_SNAPSHOT="Latest"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_SNAPSHOTS="No snapshots yet. Create one from the Content Snapshots view."
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN="Storage by Profile"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
|
||||
|
||||
; Backups view
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
||||
|
||||
@@ -198,6 +198,90 @@ class DashboardModel extends BaseDatabaseModel
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest snapshot info for the dashboard widget.
|
||||
*/
|
||||
public function getLatestSnapshot(): ?object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
try {
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuitebackup_snapshots'))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
||||
->order($db->quoteName('created') . ' DESC');
|
||||
$db->setQuery($query, 0, 1);
|
||||
|
||||
return $db->loadObject() ?: null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get snapshot count.
|
||||
*/
|
||||
public function getSnapshotCount(): int
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
try {
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokosuitebackup_snapshots'));
|
||||
$db->setQuery($query);
|
||||
|
||||
return (int) $db->loadResult();
|
||||
} catch (\Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup size trend data for the last 30 days.
|
||||
* Returns array of {date, total_size, count, status} grouped by day.
|
||||
*/
|
||||
public function getBackupTrend(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$cutoff = date('Y-m-d', strtotime('-30 days'));
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('DATE(' . $db->quoteName('backupstart') . ') AS backup_date')
|
||||
->select('SUM(' . $db->quoteName('total_size') . ') AS day_size')
|
||||
->select('COUNT(*) AS day_count')
|
||||
->select('SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('fail') . ' THEN 1 ELSE 0 END) AS fail_count')
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where('DATE(' . $db->quoteName('backupstart') . ') >= ' . $db->quote($cutoff))
|
||||
->group('DATE(' . $db->quoteName('backupstart') . ')')
|
||||
->order('backup_date ASC');
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage breakdown by profile.
|
||||
*/
|
||||
public function getStorageByProfile(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('p.title AS profile_title')
|
||||
->select('COUNT(*) AS backup_count')
|
||||
->select('COALESCE(SUM(r.total_size), 0) AS total_size')
|
||||
->from($db->quoteName('#__mokosuitebackup_records', 'r'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
||||
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
|
||||
->group($db->quoteName('r.profile_id'))
|
||||
->order('total_size DESC');
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get published backup profiles for the quick-action selector.
|
||||
*
|
||||
|
||||
@@ -24,18 +24,26 @@ class HtmlView extends BaseHtmlView
|
||||
public array $systemHealth = [];
|
||||
public array $profiles = [];
|
||||
public bool $defaultDirWarning = false;
|
||||
public ?object $latestSnapshot = null;
|
||||
public int $snapshotCount = 0;
|
||||
public array $backupTrend = [];
|
||||
public array $storageByProfile = [];
|
||||
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
/** @var \Joomla\Component\MokoSuiteBackup\Administrator\Model\DashboardModel $model */
|
||||
$model = $this->getModel();
|
||||
|
||||
$this->lastBackup = $model->getLastBackup();
|
||||
$this->nextScheduled = $model->getNextScheduled();
|
||||
$this->stats = $model->getStats();
|
||||
$this->systemHealth = $model->getSystemHealth();
|
||||
$this->profiles = $model->getProfiles();
|
||||
$this->lastBackup = $model->getLastBackup();
|
||||
$this->nextScheduled = $model->getNextScheduled();
|
||||
$this->stats = $model->getStats();
|
||||
$this->systemHealth = $model->getSystemHealth();
|
||||
$this->profiles = $model->getProfiles();
|
||||
$this->defaultDirWarning = $model->isUsingDefaultBackupDir();
|
||||
$this->latestSnapshot = $model->getLatestSnapshot();
|
||||
$this->snapshotCount = $model->getSnapshotCount();
|
||||
$this->backupTrend = $model->getBackupTrend();
|
||||
$this->storageByProfile = $model->getStorageByProfile();
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
|
||||
@@ -109,6 +109,122 @@ document.querySelectorAll('.mb-tile').forEach(function(tile) {
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Row 1b: Snapshot Widget -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<span class="icon-camera" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_SNAPSHOTS'); ?>
|
||||
</h5>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=snapshots'); ?>" class="btn btn-sm btn-outline-secondary">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_VIEW_ALL'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($this->latestSnapshot) : ?>
|
||||
<?php $types = json_decode($this->latestSnapshot->content_types, true) ?: []; ?>
|
||||
<p class="mb-1">
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_LATEST_SNAPSHOT'); ?>:</strong>
|
||||
<?php echo $this->escape($this->latestSnapshot->description); ?>
|
||||
</p>
|
||||
<p class="mb-1 text-muted">
|
||||
<?php echo HTMLHelper::_('date', $this->latestSnapshot->created, Text::_('DATE_FORMAT_LC4')); ?>
|
||||
—
|
||||
<?php foreach ($types as $type) : ?>
|
||||
<span class="badge bg-secondary"><?php echo $this->escape($type); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<small class="text-muted">
|
||||
<?php echo (int) $this->latestSnapshot->articles_count; ?> articles,
|
||||
<?php echo (int) $this->latestSnapshot->categories_count; ?> categories,
|
||||
<?php echo (int) $this->latestSnapshot->modules_count; ?> modules
|
||||
— <?php echo $this->snapshotCount; ?> total snapshots
|
||||
</small>
|
||||
</p>
|
||||
<?php else : ?>
|
||||
<p class="text-muted mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_SNAPSHOTS'); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Breakdown by Profile -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN'); ?>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($this->storageByProfile)) : ?>
|
||||
<?php
|
||||
$maxSize = max(array_column($this->storageByProfile, 'total_size')) ?: 1;
|
||||
$colors = ['#0d6efd', '#198754', '#ffc107', '#dc3545', '#6f42c1', '#0dcaf0'];
|
||||
?>
|
||||
<?php foreach ($this->storageByProfile as $i => $profile) : ?>
|
||||
<?php $pct = round(($profile->total_size / $maxSize) * 100); ?>
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-content-between small">
|
||||
<span><?php echo $this->escape($profile->profile_title ?: 'Unknown'); ?> (<?php echo (int) $profile->backup_count; ?>)</span>
|
||||
<span><?php echo HTMLHelper::_('number.bytes', $profile->total_size); ?></span>
|
||||
</div>
|
||||
<div style="background:#e9ecef; border-radius:3px; height:8px; overflow:hidden;">
|
||||
<div style="width:<?php echo $pct; ?>%; height:100%; background:<?php echo $colors[$i % count($colors)]; ?>; border-radius:3px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<p class="text-muted mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup Trend (30 days) -->
|
||||
<?php if (!empty($this->backupTrend)) : ?>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<span class="icon-chart" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND'); ?>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php
|
||||
$maxDaySize = max(array_column($this->backupTrend, 'day_size')) ?: 1;
|
||||
?>
|
||||
<div style="display:flex; align-items:flex-end; gap:2px; height:120px; overflow-x:auto;">
|
||||
<?php foreach ($this->backupTrend as $day) : ?>
|
||||
<?php
|
||||
$barHeight = max(4, round(($day->day_size / $maxDaySize) * 100));
|
||||
$barColor = $day->fail_count > 0 ? '#dc3545' : '#198754';
|
||||
$tooltip = date('M j', strtotime($day->backup_date))
|
||||
. ' — ' . $day->day_count . ' backup(s), '
|
||||
. number_format($day->day_size / 1048576, 1) . ' MB'
|
||||
. ($day->fail_count > 0 ? ', ' . $day->fail_count . ' failed' : '');
|
||||
?>
|
||||
<div style="flex:1; min-width:8px; max-width:24px; height:<?php echo $barHeight; ?>%; background:<?php echo $barColor; ?>; border-radius:2px 2px 0 0; cursor:default;"
|
||||
title="<?php echo htmlspecialchars($tooltip); ?>"></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-muted"><?php echo date('M j', strtotime('-30 days')); ?></small>
|
||||
<small class="text-muted"><?php echo date('M j'); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Row 2: Quick Actions -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
|
||||
Reference in New Issue
Block a user