Update admin dashboard

This commit is contained in:
DariusIII
2026-04-27 17:00:41 +02:00
parent e07a3056a8
commit b1a6541033
13 changed files with 958 additions and 708 deletions
+266 -127
View File
@@ -1,78 +1,78 @@
/**
* Alpine.data('adminDashboard') - Admin dashboard charts with widget auto-refresh.
* Alpine.data('adminDashboard') - Admin dashboard with deferred widget loading.
*
* The Blade view ships only the lightweight stat tiles + registration status
* synchronously. All heavy widgets (User Statistics, System Resources history,
* Recent Activity, Site Status, System Status) render skeleton placeholders
* and are populated by fetching the cached `admin.api.dashboard-data` payload
* on mount. The cache is warmed every minute by the scheduler so this fetch
* is essentially free.
*/
import Alpine from '@alpinejs/csp';
import Chart from 'chart.js/auto';
const STATUS_BADGE_CLASSES = {
operational: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
degraded: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
maintenance: 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
partial_outage: 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
major_outage: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
};
const IMPACT_BADGE_CLASSES = {
critical: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
major: 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
minor: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
};
const SYSTEM_SERVICE_SLUGS = ['database', 'redis', 'queue', 'disk'];
const formatNumber = (value) => Number(value || 0).toLocaleString();
const escapeHtml = (str) => String(str ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const badgeClass = (status) => 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full '
+ (STATUS_BADGE_CLASSES[status] ?? 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200');
Alpine.data('adminDashboard', () => ({
_charts: [],
_refreshInterval: null,
_visibilityHandler: null,
_lastRefreshAt: Date.now(),
_isRefreshing: false,
init() {
this._initCharts();
this._loadDashboardData();
this._scheduleRefresh();
},
_scheduleRefresh() {
const url = this.$el.dataset.refreshUrl;
const interval = Number.parseInt(this.$el.dataset.refreshInterval ?? '', 10) || (15 * 60 * 1000);
if (!url) {
return;
}
this._refreshInterval = window.setInterval(() => this._refreshDashboard(url), interval);
this._refreshInterval = window.setInterval(() => this._loadDashboardData(), interval);
this._visibilityHandler = () => {
if (!document.hidden && Date.now() - this._lastRefreshAt >= interval) {
this._refreshDashboard(url);
this._loadDashboardData();
}
};
document.addEventListener('visibilitychange', this._visibilityHandler);
},
_refreshDashboard(url) {
if (this._isRefreshing) {
_loadDashboardData() {
const url = this.$el.dataset.dataUrl;
if (!url || this._isRefreshing) {
return;
}
const currentContent = this.$el.querySelector('[data-dashboard-content]');
if (!currentContent) {
return;
}
this._isRefreshing = true;
fetch(url, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'text/html',
},
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
credentials: 'same-origin',
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to refresh dashboard.');
}
return response.text();
if (!response.ok) throw new Error('Failed to load dashboard data.');
return response.json();
})
.then(html => {
const parsedDocument = new DOMParser().parseFromString(html, 'text/html');
const nextContent = parsedDocument.querySelector('#adminDashboard [data-dashboard-content]');
if (!nextContent) {
return;
}
this._destroyCharts();
currentContent.innerHTML = nextContent.innerHTML;
this._initCharts();
.then(payload => {
this._renderUserStats(payload.userStats);
this._renderSystemMetrics(payload.systemMetrics);
this._renderRecentActivity(payload.recent_activity);
this._renderSiteStatus(payload.serviceStatuses, payload.activeIncidents);
this._renderSystemServices(payload.serviceStatuses);
this._lastRefreshAt = Date.now();
})
.catch(() => {})
@@ -80,40 +80,222 @@ Alpine.data('adminDashboard', () => ({
this._isRefreshing = false;
});
},
_initCharts() {
this._destroyCharts();
this._initChart('downloadsChart', 'bar', 'Downloads', 'rgba(34,197,94,0.7)', 'rgba(34,197,94,1)', false);
this._initChart('apiHitsChart', 'line', 'API Hits', 'rgba(168,85,247,0.2)', 'rgba(168,85,247,1)', true);
this._initChart('downloadsMinuteChart', 'line', 'Downloads', 'rgba(34,197,94,0.2)', 'rgba(34,197,94,1)', true);
this._initChart('apiHitsMinuteChart', 'line', 'API Hits', 'rgba(168,85,247,0.2)', 'rgba(168,85,247,1)', true);
this._initPercentChart('cpuHistory24hChart', 'CPU Usage %', 'rgba(249,115,22,0.2)', 'rgba(249,115,22,1)');
this._initPercentChart('ramHistory24hChart', 'RAM Usage %', 'rgba(6,182,212,0.2)', 'rgba(6,182,212,1)');
this._initPercentChart('cpuHistory30dChart', 'CPU Usage %', 'rgba(249,115,22,0.2)', 'rgba(249,115,22,1)');
this._initPercentChart('ramHistory30dChart', 'RAM Usage %', 'rgba(6,182,212,0.2)', 'rgba(6,182,212,1)');
_swapWidget(name) {
const widget = this.$el.querySelector(`[data-widget="${name}"]`);
if (!widget) return null;
widget.querySelector('[data-widget-loading]')?.classList.add('hidden');
const content = widget.querySelector('[data-widget-content]');
content?.classList.remove('hidden');
return widget;
},
_parseChartData(rawData) {
try {
return JSON.parse(rawData || '[]');
} catch {
return [];
_renderUserStats(stats) {
if (!stats) return;
const widget = this._swapWidget('user-stats');
if (!widget) return;
const summary = stats.summary ?? {};
const cards = [
{ label: 'Total Users', value: summary.total_users, theme: 'blue' },
{ label: 'Downloads Today', value: summary.downloads_today, theme: 'green' },
{ label: 'Downloads (7d)', value: summary.downloads_week, theme: 'purple' },
{ label: 'API Hits Today', value: summary.api_hits_today, theme: 'orange' },
{ label: 'API Hits (7d)', value: summary.api_hits_week, theme: 'pink' },
];
const summaryGrid = widget.querySelector('[data-summary-grid]');
if (summaryGrid) {
summaryGrid.innerHTML = cards.map(c => `
<div class="bg-linear-to-br from-${c.theme}-50 to-${c.theme}-100 dark:from-${c.theme}-900 dark:to-${c.theme}-800 rounded-lg p-4 text-center">
<p class="text-sm text-${c.theme}-600 dark:text-${c.theme}-300 font-medium mb-1">${escapeHtml(c.label)}</p>
<p class="text-2xl font-bold text-${c.theme}-800 dark:text-${c.theme}-100">${formatNumber(c.value)}</p>
</div>
`).join('');
}
const roles = stats.users_by_role ?? [];
const totalUsers = roles.reduce((sum, r) => sum + Number(r.count || 0), 0);
const rolesTbody = widget.querySelector('[data-roles-tbody]');
if (rolesTbody) {
rolesTbody.innerHTML = roles.length === 0
? '<tr><td colspan="3" class="px-4 py-3 text-sm text-center text-gray-500 dark:text-gray-400">No data available</td></tr>'
: roles.map(r => {
const pct = totalUsers > 0 ? ((Number(r.count) / totalUsers) * 100).toFixed(1) : '0';
const role = String(r.role ?? '');
const roleLabel = role ? role.charAt(0).toUpperCase() + role.slice(1) : '';
return `
<tr class="hover:bg-gray-100 dark:hover:bg-gray-800 transition">
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">${escapeHtml(roleLabel)}</span>
</td>
<td class="px-4 py-3 text-sm text-right text-gray-900 dark:text-gray-100 font-semibold">${formatNumber(r.count)}</td>
<td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-400">${pct}%</td>
</tr>`;
}).join('');
}
const downloaders = stats.top_downloaders ?? [];
const downloadersTbody = widget.querySelector('[data-downloaders-tbody]');
if (downloadersTbody) {
const medalIcons = ['fa-medal text-yellow-500', 'fa-medal text-gray-400', 'fa-medal text-orange-600'];
downloadersTbody.innerHTML = downloaders.length === 0
? '<tr><td colspan="3" class="px-4 py-3 text-sm text-center text-gray-500 dark:text-gray-400">No downloads in the last 7 days</td></tr>'
: downloaders.map((d, i) => {
const rank = i < 3 ? `<i class="fas ${medalIcons[i]} text-lg"></i>` : `<span class="text-gray-500">${i + 1}</span>`;
return `
<tr class="hover:bg-gray-100 dark:hover:bg-gray-800 transition">
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">${rank}</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">${escapeHtml(d.username)}</td>
<td class="px-4 py-3 text-sm text-right text-gray-900 dark:text-gray-100 font-semibold">${formatNumber(d.download_count)}</td>
</tr>`;
}).join('');
}
this._renderChart('downloadsChart', 'bar', 'Downloads', stats.downloads_per_hour ?? [],
'rgba(34,197,94,0.7)', 'rgba(34,197,94,1)', false);
this._renderChart('apiHitsChart', 'line', 'API Hits', stats.api_hits_per_hour ?? [],
'rgba(168,85,247,0.2)', 'rgba(168,85,247,1)', true);
this._renderChart('downloadsMinuteChart', 'line', 'Downloads', stats.downloads_per_minute ?? [],
'rgba(34,197,94,0.2)', 'rgba(34,197,94,1)', true);
this._renderChart('apiHitsMinuteChart', 'line', 'API Hits', stats.api_hits_per_minute ?? [],
'rgba(168,85,247,0.2)', 'rgba(168,85,247,1)', true);
},
_renderSystemMetrics(metrics) {
if (!metrics) return;
const widget = this._swapWidget('system-metrics');
if (!widget) return;
const setText = (selector, text) => {
const el = widget.querySelector(`[data-metric="${selector}"]`);
if (el) el.textContent = text;
};
const cpu = metrics.cpu ?? {};
const ram = metrics.ram ?? {};
const load = cpu.load_average ?? {};
setText('cpu-current', `${Number(cpu.current ?? 0).toFixed(1)}%`);
setText('cpu-cores', cpu.cores ?? '—');
setText('cpu-threads', cpu.threads ?? '—');
setText('cpu-load-1min', load['1min'] ?? '—');
setText('cpu-load-5min', load['5min'] ?? '—');
setText('cpu-load-15min', load['15min'] ?? '—');
const cpuModel = widget.querySelector('[data-metric="cpu-model"]');
if (cpuModel && cpu.model && cpu.model !== 'Unknown') {
cpuModel.title = cpu.model;
cpuModel.innerHTML = `<i class="fas fa-info-circle mr-1"></i>${escapeHtml(cpu.model)}`;
}
setText('ram-current', `${Number(ram.percentage ?? 0).toFixed(1)}%`);
setText('ram-details', `${Number(ram.used ?? 0).toFixed(2)} GB / ${Number(ram.total ?? 0).toFixed(2)} GB`);
this._renderPercentChart('cpuHistory24hChart', 'CPU Usage %', cpu.history_24h ?? [],
'rgba(249,115,22,0.2)', 'rgba(249,115,22,1)');
this._renderPercentChart('ramHistory24hChart', 'RAM Usage %', ram.history_24h ?? [],
'rgba(6,182,212,0.2)', 'rgba(6,182,212,1)');
this._renderPercentChart('cpuHistory30dChart', 'CPU Usage %', cpu.history_30d ?? [],
'rgba(249,115,22,0.2)', 'rgba(249,115,22,1)');
this._renderPercentChart('ramHistory30dChart', 'RAM Usage %', ram.history_30d ?? [],
'rgba(6,182,212,0.2)', 'rgba(6,182,212,1)');
},
_renderRecentActivity(activities) {
const widget = this._swapWidget('recent-activity');
if (!widget) return;
const updated = widget.querySelector('[data-metric="activity-updated"]');
if (updated) {
updated.textContent = `Updated ${new Date().toLocaleTimeString()}`;
}
const container = widget.querySelector('#recent-activity-container');
if (!container) return;
if (!Array.isArray(activities) || activities.length === 0) {
container.innerHTML = '<p class="text-gray-500 dark:text-gray-400 text-center py-4">No recent activity</p>';
return;
}
container.innerHTML = activities.map(activity => {
let extra = '';
if (activity.type === 'deleted' && activity.metadata && activity.metadata.deleted_by) {
const permanent = activity.metadata.permanent ? ', permanent' : '';
extra = `<span class="text-xs text-gray-500 dark:text-gray-400"> (by ${escapeHtml(activity.metadata.deleted_by)}${escapeHtml(permanent)})</span>`;
}
return `
<div class="flex items-start activity-item rounded-lg p-2 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors">
<div class="w-8 h-8 ${activity.icon_bg} rounded-full flex items-center justify-center mr-3 shrink-0">
<i class="fas fa-${escapeHtml(activity.icon)} ${activity.icon_color} text-sm"></i>
</div>
<div class="flex-1">
<p class="text-sm text-gray-800 dark:text-gray-200">${escapeHtml(activity.message)}${extra}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">${escapeHtml(activity.created_at_human ?? '')}</p>
</div>
</div>`;
}).join('');
},
_renderSiteStatus(services, incidents) {
const widget = this._swapWidget('site-status');
if (!widget) return;
const grid = widget.querySelector('[data-services-grid]');
if (grid) {
grid.innerHTML = (services ?? []).map(svc => `
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-100 dark:border-gray-700">
<div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">${escapeHtml(svc.name)}</span>
<span class="block text-xs text-gray-500 dark:text-gray-400">${Number(svc.uptime_percentage).toFixed(2)}% uptime</span>
</div>
<span class="${badgeClass(svc.status)}">${escapeHtml(svc.status_label)}</span>
</div>
`).join('');
}
const region = widget.querySelector('[data-incidents-region]');
if (!region) return;
if (!Array.isArray(incidents) || incidents.length === 0) {
region.innerHTML = '<p class="text-sm text-green-600 dark:text-green-400"><i class="fas fa-check-circle mr-1"></i>No active incidents</p>';
return;
}
const visible = incidents.slice(0, 5);
const overflow = incidents.length - visible.length;
const items = visible.map(inc => {
const impactClass = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full shrink-0 '
+ (IMPACT_BADGE_CLASSES[inc.impact] ?? 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200');
const autoBadge = inc.is_auto
? '<span class="ml-1 px-1 py-0.5 text-[10px] leading-3 font-semibold rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200">Auto</span>'
: '';
const title = String(inc.title ?? '');
const truncated = title.length > 50 ? `${title.slice(0, 50)}` : title;
return `
<div class="flex items-start justify-between gap-2 text-sm py-1">
<div class="min-w-0">
<span class="text-gray-800 dark:text-gray-200 truncate block">${escapeHtml(truncated)}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">${escapeHtml((inc.services ?? []).join(', '))} &middot; ${escapeHtml(inc.started_at_human ?? '')}${autoBadge}</span>
</div>
<span class="${impactClass}">${escapeHtml(inc.impact_label)}</span>
</div>`;
}).join('');
region.innerHTML = `
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">
<i class="fas fa-exclamation-triangle mr-1"></i>${incidents.length} Active Incident${incidents.length === 1 ? '' : 's'}
</h4>
<div class="space-y-2">${items}${overflow > 0 ? `<p class="text-xs text-gray-500 dark:text-gray-400 pt-1">+ ${overflow} more</p>` : ''}</div>
`;
},
_renderSystemServices(services) {
const widget = this._swapWidget('system-status');
if (!widget) return;
const container = widget.querySelector('[data-system-services]');
if (!container) return;
const bySlug = {};
(services ?? []).forEach(svc => { bySlug[svc.slug] = svc; });
container.innerHTML = SYSTEM_SERVICE_SLUGS.map(slug => {
const svc = bySlug[slug];
const label = svc?.name ?? (slug.charAt(0).toUpperCase() + slug.slice(1));
const right = svc
? `<span class="${badgeClass(svc.status)}">${escapeHtml(svc.status_label)}</span>`
: '<span class="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full text-sm">Not configured</span>';
return `
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">${escapeHtml(label)}</span>
${right}
</div>`;
}).join('');
},
_destroyChart(canvasId) {
const existing = this._charts.find(c => c.canvas?.id === canvasId);
if (existing) {
existing.destroy();
this._charts = this._charts.filter(c => c !== existing);
}
},
_initChart(canvasId, type, label, backgroundColor, borderColor, fill) {
_renderChart(canvasId, type, label, data, backgroundColor, borderColor, fill) {
const canvas = document.getElementById(canvasId);
if (!canvas) {
return;
}
const data = this._parseChartData(canvas.dataset.chartData || canvas.dataset.history);
if (data.length === 0) {
return;
}
if (!canvas || !Array.isArray(data) || data.length === 0) return;
this._destroyChart(canvasId);
const chart = new Chart(canvas, {
type,
data: {
@@ -131,38 +313,16 @@ Alpine.data('adminDashboard', () => ({
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0,
},
},
},
plugins: {
legend: {
display: false,
},
},
scales: { y: { beginAtZero: true, ticks: { precision: 0 } } },
plugins: { legend: { display: false } },
},
});
this._charts.push(chart);
},
_initPercentChart(canvasId, label, backgroundColor, borderColor) {
_renderPercentChart(canvasId, label, data, backgroundColor, borderColor) {
const canvas = document.getElementById(canvasId);
if (!canvas) {
return;
}
const data = this._parseChartData(canvas.dataset.history);
if (data.length === 0) {
return;
}
if (!canvas || !Array.isArray(data) || data.length === 0) return;
this._destroyChart(canvasId);
const chart = new Chart(canvas, {
type: 'line',
data: {
@@ -180,40 +340,19 @@ Alpine.data('adminDashboard', () => ({
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: value => value + '%',
},
},
},
plugins: {
legend: {
display: false,
},
},
scales: { y: { beginAtZero: true, max: 100, ticks: { callback: value => value + '%' } } },
plugins: { legend: { display: false } },
},
});
this._charts.push(chart);
},
_destroyCharts() {
this._charts.forEach(chart => chart.destroy());
this._charts = [];
},
destroy() {
if (this._refreshInterval) {
window.clearInterval(this._refreshInterval);
}
if (this._visibilityHandler) {
document.removeEventListener('visibilitychange', this._visibilityHandler);
}
if (this._refreshInterval) window.clearInterval(this._refreshInterval);
if (this._visibilityHandler) document.removeEventListener('visibilitychange', this._visibilityHandler);
this._destroyCharts();
},
}));
+169 -400
View File
@@ -15,6 +15,7 @@
<div x-data="adminDashboard"
id="adminDashboard"
data-refresh-url="{{ route('admin.index') }}"
data-data-url="{{ route('admin.api.dashboard-data') }}"
data-refresh-interval="{{ 15 * 60 * 1000 }}">
<div class="space-y-6" data-dashboard-content>
<!-- Welcome Section -->
@@ -152,8 +153,9 @@
</div>
</div>
<!-- Site Status & Active Incidents -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<!-- Site Status & Active Incidents (deferred populated by Alpine on mount) -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700"
data-widget="site-status">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center justify-between">
<span>
<i class="fas fa-signal mr-2 text-blue-600 dark:text-blue-400"></i>
@@ -161,75 +163,15 @@
</span>
<a href="{{ route('admin.status.index') }}" class="text-xs text-blue-600 dark:text-blue-400 hover:underline font-normal">Manage &rarr;</a>
</h3>
@php
$dashBadge = static function (string $status): string {
$base = 'px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full';
return $base.' '.match ($status) {
'operational' => 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
'degraded' => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
'maintenance' => 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
'partial_outage' => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
'major_outage' => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
default => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
};
};
@endphp
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
@foreach($serviceStatuses as $svc)
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-100 dark:border-gray-700">
<div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ $svc->name }}</span>
<span class="block text-xs text-gray-500 dark:text-gray-400">{{ number_format((float) $svc->uptime_percentage, 2) }}% uptime</span>
</div>
<span class="{{ $dashBadge($svc->status->value) }}">{{ $svc->status->label() }}</span>
</div>
@endforeach
<div data-widget-loading class="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
<div class="h-14 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
<div class="h-14 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
<div class="h-14 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
</div>
<div data-widget-content class="hidden">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4" data-services-grid></div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-3" data-incidents-region></div>
</div>
@if($activeIncidents->isNotEmpty())
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<h4 class="text-sm font-semibold text-red-600 dark:text-red-400 mb-2">
<i class="fas fa-exclamation-triangle mr-1"></i>
{{ $activeIncidents->count() }} Active {{ Str::plural('Incident', $activeIncidents->count()) }}
</h4>
<div class="space-y-2">
@foreach($activeIncidents->take(5) as $incident)
<div class="flex items-start justify-between gap-2 text-sm py-1">
<div class="min-w-0">
<span class="text-gray-800 dark:text-gray-200 truncate block">{{ Str::limit($incident->title, 50) }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ $incident->services->pluck('name')->join(', ') }}
&middot; {{ $incident->started_at->diffForHumans() }}
@if($incident->is_auto)
<span class="ml-1 px-1 py-0.5 text-[10px] leading-3 font-semibold rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200">Auto</span>
@endif
</span>
</div>
<span class="px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full shrink-0 {{ match($incident->impact->value) {
'critical' => 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200',
'major' => 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
'minor' => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
default => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
} }}">{{ $incident->impact->label() }}</span>
</div>
@endforeach
@if($activeIncidents->count() > 5)
<p class="text-xs text-gray-500 dark:text-gray-400 pt-1">
+ {{ $activeIncidents->count() - 5 }} more &mdash;
<a href="{{ route('admin.status.index') }}" class="text-blue-600 dark:text-blue-400 hover:underline">view all</a>
</p>
@endif
</div>
</div>
@else
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-sm text-green-600 dark:text-green-400">
<i class="fas fa-check-circle mr-1"></i>No active incidents
</p>
</div>
@endif
</div>
<!-- Registration Status Widget -->
@@ -309,395 +251,222 @@
</div>
</div>
<!-- User Statistics Widget -->
@if(isset($userStats))
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<!-- User Statistics Widget (deferred) -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700"
data-widget="user-stats">
<h3 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-6 flex items-center">
<i class="fas fa-chart-line mr-2 text-blue-600 dark:text-blue-400"></i>
User Statistics
</h3>
<!-- Summary Stats Row -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
<div class="bg-linear-to-br from-blue-50 to-blue-100 dark:from-blue-900 dark:to-blue-800 rounded-lg p-4 text-center">
<p class="text-sm text-blue-600 dark:text-blue-300 font-medium mb-1">Total Users</p>
<p class="text-2xl font-bold text-blue-800 dark:text-blue-100">{{ number_format($userStats['summary']['total_users']) }}</p>
<div data-widget-loading class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
@for($i = 0; $i < 5; $i++)
<div class="h-24 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
@endfor
</div>
<div class="bg-linear-to-br from-green-50 to-green-100 dark:from-green-900 dark:to-green-800 rounded-lg p-4 text-center">
<p class="text-sm text-green-600 dark:text-green-300 font-medium mb-1">Downloads Today</p>
<p class="text-2xl font-bold text-green-800 dark:text-green-100">{{ number_format($userStats['summary']['downloads_today']) }}</p>
</div>
<div class="bg-linear-to-br from-purple-50 to-purple-100 dark:from-purple-900 dark:to-purple-800 rounded-lg p-4 text-center">
<p class="text-sm text-purple-600 dark:text-purple-300 font-medium mb-1">Downloads (7d)</p>
<p class="text-2xl font-bold text-purple-800 dark:text-purple-100">{{ number_format($userStats['summary']['downloads_week']) }}</p>
</div>
<div class="bg-linear-to-br from-orange-50 to-orange-100 dark:from-orange-900 dark:to-orange-800 rounded-lg p-4 text-center">
<p class="text-sm text-orange-600 dark:text-orange-300 font-medium mb-1">API Hits Today</p>
<p class="text-2xl font-bold text-orange-800 dark:text-orange-100">{{ number_format($userStats['summary']['api_hits_today']) }}</p>
</div>
<div class="bg-linear-to-br from-pink-50 to-pink-100 dark:from-pink-900 dark:to-pink-800 rounded-lg p-4 text-center">
<p class="text-sm text-pink-600 dark:text-pink-300 font-medium mb-1">API Hits (7d)</p>
<p class="text-2xl font-bold text-pink-800 dark:text-pink-100">{{ number_format($userStats['summary']['api_hits_week']) }}</p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@for($i = 0; $i < 4; $i++)
<div class="h-64 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
@endfor
</div>
</div>
<div data-widget-content class="hidden">
<!-- Summary Stats Row -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6" data-summary-grid></div>
<!-- Tables and Graphs Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Users by Role Table -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-user-tag mr-2 text-indigo-600 dark:text-indigo-400"></i>
Users by Role
</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Role</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Count</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Percentage</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@php
$totalUsers = collect($userStats['users_by_role'])->sum('count');
@endphp
@foreach($userStats['users_by_role'] as $roleData)
<tr class="hover:bg-gray-100 dark:hover:bg-gray-800 transition">
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
{{ ucfirst($roleData['role']) }}
</span>
</td>
<td class="px-4 py-3 text-sm text-right text-gray-900 dark:text-gray-100 font-semibold">
{{ number_format($roleData['count']) }}
</td>
<td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-400">
{{ $totalUsers > 0 ? number_format(($roleData['count'] / $totalUsers) * 100, 1) : 0 }}%
</td>
</tr>
@endforeach
@if(empty($userStats['users_by_role']))
<tr>
<td colspan="3" class="px-4 py-3 text-sm text-center text-gray-500 dark:text-gray-400">
No data available
</td>
</tr>
@endif
</tbody>
</table>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Users by Role Table -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-user-tag mr-2 text-indigo-600 dark:text-indigo-400"></i>
Users by Role
</h4>
<div class="overflow-x-auto"><table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead><tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Role</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Count</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Percentage</th>
</tr></thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700" data-roles-tbody></tbody>
</table></div>
</div>
</div>
<!-- Top Downloaders Table -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-trophy mr-2 text-yellow-600 dark:text-yellow-400"></i>
Top Downloaders (Last 7 Days)
</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Rank</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Username</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Downloads</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($userStats['top_downloaders'] as $index => $downloader)
<tr class="hover:bg-gray-100 dark:hover:bg-gray-800 transition">
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
@if($index === 0)
<i class="fas fa-medal text-yellow-500 text-lg"></i>
@elseif($index === 1)
<i class="fas fa-medal text-gray-400 text-lg"></i>
@elseif($index === 2)
<i class="fas fa-medal text-orange-600 text-lg"></i>
@else
<span class="text-gray-500">{{ $index + 1 }}</span>
@endif
</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $downloader['username'] }}
</td>
<td class="px-4 py-3 text-sm text-right text-gray-900 dark:text-gray-100 font-semibold">
{{ number_format($downloader['download_count']) }}
</td>
</tr>
@endforeach
@if(empty($userStats['top_downloaders']))
<tr>
<td colspan="3" class="px-4 py-3 text-sm text-center text-gray-500 dark:text-gray-400">
No downloads in the last 7 days
</td>
</tr>
@endif
</tbody>
</table>
<!-- Top Downloaders Table -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-trophy mr-2 text-yellow-600 dark:text-yellow-400"></i>
Top Downloaders (Last 7 Days)
</h4>
<div class="overflow-x-auto"><table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead><tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Rank</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Username</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Downloads</th>
</tr></thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700" data-downloaders-tbody></tbody>
</table></div>
</div>
</div>
<!-- Downloads Chart -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-bar mr-2 text-green-600 dark:text-green-400"></i>
Downloads (Last 7 Days - Hourly)
</h4>
<div class="chart-container">
<canvas id="downloadsChart" data-chart-data="{{ json_encode($userStats['downloads_per_hour']) }}"></canvas>
<!-- Charts -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-bar mr-2 text-green-600 dark:text-green-400"></i>
Downloads (Last 7 Days - Hourly)
</h4>
<div class="chart-container"><canvas id="downloadsChart"></canvas></div>
</div>
</div>
<!-- API Hits Chart -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-area mr-2 text-purple-600 dark:text-purple-400"></i>
API Hits (Last 7 Days - Hourly)
</h4>
<div class="chart-container">
<canvas id="apiHitsChart" data-chart-data="{{ json_encode($userStats['api_hits_per_hour']) }}"></canvas>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-area mr-2 text-purple-600 dark:text-purple-400"></i>
API Hits (Last 7 Days - Hourly)
</h4>
<div class="chart-container"><canvas id="apiHitsChart"></canvas></div>
</div>
</div>
<!-- Downloads Per Minute Chart -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-line mr-2 text-green-600 dark:text-green-400"></i>
Downloads (Last 60 Minutes)
</h4>
<div class="chart-container">
<canvas id="downloadsMinuteChart" data-chart-data="{{ json_encode($userStats['downloads_per_minute']) }}"></canvas>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-line mr-2 text-green-600 dark:text-green-400"></i>
Downloads (Last 60 Minutes)
</h4>
<div class="chart-container"><canvas id="downloadsMinuteChart"></canvas></div>
</div>
</div>
<!-- API Hits Per Minute Chart -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-line mr-2 text-purple-600 dark:text-purple-400"></i>
API Hits (Last 60 Minutes)
</h4>
<div class="chart-container">
<canvas id="apiHitsMinuteChart" data-chart-data="{{ json_encode($userStats['api_hits_per_minute']) }}"></canvas>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
<i class="fas fa-chart-line mr-2 text-purple-600 dark:text-purple-400"></i>
API Hits (Last 60 Minutes)
</h4>
<div class="chart-container"><canvas id="apiHitsMinuteChart"></canvas></div>
</div>
</div>
</div>
</div>
@endif
<!-- System Metrics (CPU & RAM) -->
@if(isset($systemMetrics))
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<!-- System Metrics (CPU & RAM) deferred -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700"
data-widget="system-metrics">
<h3 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-6 flex items-center">
<i class="fas fa-server mr-2 text-orange-600 dark:text-orange-400"></i>
System Resources
</h3>
<!-- Current Usage Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div class="bg-linear-to-br from-orange-50 to-orange-100 dark:from-orange-900 dark:to-orange-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<div>
<p class="text-sm text-orange-600 dark:text-orange-300 font-medium mb-1">CPU Usage</p>
<p class="text-3xl font-bold text-orange-800 dark:text-orange-100" data-metric="cpu-current">{{ number_format($systemMetrics['cpu']['current'], 1) }}%</p>
</div>
<div class="w-12 h-12 bg-orange-200 dark:bg-orange-700 rounded-lg flex items-center justify-center">
<i class="fas fa-microchip text-2xl text-orange-600 dark:text-orange-300"></i>
</div>
</div>
<div class="border-t border-orange-200 dark:border-orange-700 pt-3 space-y-2">
<div class="flex justify-between text-xs">
<span class="text-orange-600 dark:text-orange-300">Cores:</span>
<span class="font-semibold text-orange-800 dark:text-orange-100">{{ $systemMetrics['cpu']['cores'] }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-orange-600 dark:text-orange-300">Threads:</span>
<span class="font-semibold text-orange-800 dark:text-orange-100">{{ $systemMetrics['cpu']['threads'] }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-orange-600 dark:text-orange-300">Load (1m):</span>
<span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-load-1min">{{ $systemMetrics['cpu']['load_average']['1min'] }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-orange-600 dark:text-orange-300">Load (5m):</span>
<span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-load-5min">{{ $systemMetrics['cpu']['load_average']['5min'] }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-orange-600 dark:text-orange-300">Load (15m):</span>
<span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-load-15min">{{ $systemMetrics['cpu']['load_average']['15min'] }}</span>
</div>
@if($systemMetrics['cpu']['model'] !== 'Unknown')
<div class="pt-2 border-t border-orange-200 dark:border-orange-700">
<p class="text-xs text-orange-600 dark:text-orange-300 truncate" title="{{ $systemMetrics['cpu']['model'] }}">
<i class="fas fa-info-circle mr-1"></i>{{ $systemMetrics['cpu']['model'] }}
</p>
</div>
@endif
</div>
<div data-widget-loading class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="h-40 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
<div class="h-40 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
</div>
<div class="bg-linear-to-br from-cyan-50 to-cyan-100 dark:from-cyan-900 dark:to-cyan-800 rounded-lg p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-cyan-600 dark:text-cyan-300 font-medium mb-1">RAM Usage</p>
<p class="text-3xl font-bold text-cyan-800 dark:text-cyan-100" data-metric="ram-current">{{ number_format($systemMetrics['ram']['percentage'], 1) }}%</p>
<p class="text-xs text-cyan-600 dark:text-cyan-300 mt-1" data-metric="ram-details">
{{ number_format($systemMetrics['ram']['used'], 2) }} GB / {{ number_format($systemMetrics['ram']['total'], 2) }} GB
</p>
</div>
<div class="w-12 h-12 bg-cyan-200 dark:bg-cyan-700 rounded-lg flex items-center justify-center">
<i class="fas fa-memory text-2xl text-cyan-600 dark:text-cyan-300"></i>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
@for($i = 0; $i < 4; $i++)
<div class="h-56 rounded-lg bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
@endfor
</div>
</div>
<!-- Last Updated Indicator -->
<div class="mb-4 text-right">
<span class="text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-sync-alt mr-1"></i>Last dashboard refresh: <span data-metric="last-updated">{{ $dashboardLastRefreshedAt }}</span>
<span class="ml-2 text-green-600 dark:text-green-400">(Auto-refreshes every 15 minutes)</span>
</span>
</div>
<!-- Historical Graphs -->
<div class="space-y-6">
<!-- 24 Hour Charts -->
<div>
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center">
<i class="fas fa-clock mr-2 text-blue-600 dark:text-blue-400"></i>
Last 24 Hours (Hour by Hour)
</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- CPU Usage 24h Graph -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<i class="fas fa-microchip mr-2 text-orange-600 dark:text-orange-400"></i>
CPU Usage
</h5>
<div class="chart-container">
<canvas id="cpuHistory24hChart"
data-history="{{ json_encode($systemMetrics['cpu']['history_24h']) }}"
data-chart-label="CPU Usage %"></canvas>
<div data-widget-content class="hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<!-- CPU card -->
<div class="bg-linear-to-br from-orange-50 to-orange-100 dark:from-orange-900 dark:to-orange-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<div>
<p class="text-sm text-orange-600 dark:text-orange-300 font-medium mb-1">CPU Usage</p>
<p class="text-3xl font-bold text-orange-800 dark:text-orange-100" data-metric="cpu-current"></p>
</div>
<div class="w-12 h-12 bg-orange-200 dark:bg-orange-700 rounded-lg flex items-center justify-center">
<i class="fas fa-microchip text-2xl text-orange-600 dark:text-orange-300"></i>
</div>
</div>
<div class="border-t border-orange-200 dark:border-orange-700 pt-3 space-y-2 text-xs">
<div class="flex justify-between"><span class="text-orange-600 dark:text-orange-300">Cores:</span><span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-cores"></span></div>
<div class="flex justify-between"><span class="text-orange-600 dark:text-orange-300">Threads:</span><span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-threads"></span></div>
<div class="flex justify-between"><span class="text-orange-600 dark:text-orange-300">Load (1m):</span><span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-load-1min"></span></div>
<div class="flex justify-between"><span class="text-orange-600 dark:text-orange-300">Load (5m):</span><span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-load-5min"></span></div>
<div class="flex justify-between"><span class="text-orange-600 dark:text-orange-300">Load (15m):</span><span class="font-semibold text-orange-800 dark:text-orange-100" data-metric="cpu-load-15min"></span></div>
<div class="pt-2 border-t border-orange-200 dark:border-orange-700">
<p class="text-xs text-orange-600 dark:text-orange-300 truncate" data-metric="cpu-model"></p>
</div>
</div>
</div>
<!-- RAM Usage 24h Graph -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<i class="fas fa-memory mr-2 text-cyan-600 dark:text-cyan-400"></i>
RAM Usage
</h5>
<div class="chart-container">
<canvas id="ramHistory24hChart"
data-history="{{ json_encode($systemMetrics['ram']['history_24h']) }}"
data-chart-label="RAM Usage %"></canvas>
<!-- RAM card -->
<div class="bg-linear-to-br from-cyan-50 to-cyan-100 dark:from-cyan-900 dark:to-cyan-800 rounded-lg p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-cyan-600 dark:text-cyan-300 font-medium mb-1">RAM Usage</p>
<p class="text-3xl font-bold text-cyan-800 dark:text-cyan-100" data-metric="ram-current"></p>
<p class="text-xs text-cyan-600 dark:text-cyan-300 mt-1" data-metric="ram-details"></p>
</div>
<div class="w-12 h-12 bg-cyan-200 dark:bg-cyan-700 rounded-lg flex items-center justify-center">
<i class="fas fa-memory text-2xl text-cyan-600 dark:text-cyan-300"></i>
</div>
</div>
</div>
</div>
<!-- 30 Day Charts -->
<div>
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center">
<i class="fas fa-calendar-alt mr-2 text-purple-600 dark:text-purple-400"></i>
Last 30 Days (Day by Day)
</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- CPU Usage 30d Graph -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<i class="fas fa-microchip mr-2 text-orange-600 dark:text-orange-400"></i>
CPU Usage
</h5>
<div class="chart-container">
<canvas id="cpuHistory30dChart"
data-history="{{ json_encode($systemMetrics['cpu']['history_30d']) }}"
data-chart-label="CPU Usage %"></canvas>
<div class="space-y-6">
<div>
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center">
<i class="fas fa-clock mr-2 text-blue-600 dark:text-blue-400"></i>
Last 24 Hours (Hour by Hour)
</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3"><i class="fas fa-microchip mr-2 text-orange-600 dark:text-orange-400"></i>CPU Usage</h5>
<div class="chart-container"><canvas id="cpuHistory24hChart" data-chart-label="CPU Usage %"></canvas></div>
</div>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3"><i class="fas fa-memory mr-2 text-cyan-600 dark:text-cyan-400"></i>RAM Usage</h5>
<div class="chart-container"><canvas id="ramHistory24hChart" data-chart-label="RAM Usage %"></canvas></div>
</div>
</div>
<!-- RAM Usage 30d Graph -->
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<i class="fas fa-memory mr-2 text-cyan-600 dark:text-cyan-400"></i>
RAM Usage
</h5>
<div class="chart-container">
<canvas id="ramHistory30dChart"
data-history="{{ json_encode($systemMetrics['ram']['history_30d']) }}"
data-chart-label="RAM Usage %"></canvas>
</div>
<div>
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center">
<i class="fas fa-calendar-alt mr-2 text-purple-600 dark:text-purple-400"></i>
Last 30 Days (Day by Day)
</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3"><i class="fas fa-microchip mr-2 text-orange-600 dark:text-orange-400"></i>CPU Usage</h5>
<div class="chart-container"><canvas id="cpuHistory30dChart" data-chart-label="CPU Usage %"></canvas></div>
</div>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h5 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-3"><i class="fas fa-memory mr-2 text-cyan-600 dark:text-cyan-400"></i>RAM Usage</h5>
<div class="chart-container"><canvas id="ramHistory30dChart" data-chart-label="RAM Usage %"></canvas></div>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- System Status -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<!-- System Status (deferred) -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700"
data-widget="system-status">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">System Status</h3>
@php
$systemServiceSlugs = ['database', 'redis', 'queue', 'disk'];
$systemServices = $serviceStatuses->keyBy('slug');
@endphp
<div class="space-y-3">
@foreach($systemServiceSlugs as $slug)
@php $svc = $systemServices->get($slug); @endphp
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ $svc?->name ?? ucfirst($slug) }}</span>
@if($svc)
<span class="{{ $dashBadge($svc->status->value) }}">
{{ $svc->status->label() }}
</span>
@else
<span class="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full text-sm">
Not configured
</span>
@endif
</div>
@endforeach
<div data-widget-loading class="space-y-3">
@for($i = 0; $i < 4; $i++)
<div class="h-6 rounded bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
@endfor
</div>
<div data-widget-content class="hidden space-y-3" data-system-services></div>
</div>
<!-- Recent Activity -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<!-- Recent Activity (deferred) -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700"
data-widget="recent-activity">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center justify-between">
<span>
<i class="fas fa-history mr-2 text-indigo-600 dark:text-indigo-400"></i>
Recent User Activity
</span>
<span class="text-xs text-gray-500 dark:text-gray-400" id="activity-last-updated">
<i class="fas fa-sync-alt"></i> Last dashboard refresh: {{ $dashboardLastRefreshedAt }}
<i class="fas fa-sync-alt"></i> <span data-metric="activity-updated">loading…</span>
</span>
</h3>
<div class="space-y-3" id="recent-activity-container">
@if(isset($recent_activity) && count($recent_activity) > 0)
@foreach($recent_activity as $activity)
<div class="flex items-start activity-item rounded-lg p-2 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors">
<div class="w-8 h-8 {{ $activity->icon_bg }} rounded-full flex items-center justify-center mr-3 shrink-0">
<i class="fas fa-{{ $activity->icon }} {{ $activity->icon_color }} text-sm"></i>
</div>
<div class="flex-1">
<p class="text-sm text-gray-800 dark:text-gray-200">
{{ $activity->message }}
@if($activity->type === 'deleted' && isset($activity->metadata['deleted_by']))
<span class="text-xs text-gray-500 dark:text-gray-400">
(by {{ $activity->metadata['deleted_by'] }}{{ isset($activity->metadata['permanent']) ? ', permanent' : '' }})
</span>
@endif
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $activity->created_at->diffForHumans() }}</p>
</div>
</div>
@endforeach
@else
<p class="text-gray-500 dark:text-gray-400 text-center py-4" id="no-activity-message">No recent activity</p>
@endif
<div data-widget-loading class="space-y-3">
@for($i = 0; $i < 5; $i++)
<div class="h-10 rounded bg-gray-100 dark:bg-gray-900 animate-pulse"></div>
@endfor
</div>
<div data-widget-content class="hidden space-y-3" id="recent-activity-container"></div>
</div>
</div>