feat(system-hub): enhance dynamic overview stats for v0.3.2

- Add network throughput stats (RX/TX total bytes)
- Add process count display (running/total)
- Add swap usage information
- Add dynamic status indicators (✓, , ⚠️) with pulse animation
- Add detailed tooltips with absolute values
- Add detail text under each stat card
- Enhance stats grid layout for 5 cards
- Update version from 0.3.1 to 0.3.2

Backend enhancements:
- Extract process count from /proc/loadavg
- Calculate swap usage from /proc/meminfo
- Aggregate network throughput from all interfaces

Frontend enhancements:
- Display process count alongside CPU load
- Show swap usage when available
- Display total RX/TX in GB
- Add pulsing status icons
- Show contextual details (MB/GB values, process count, load average)

CSS improvements:
- Add .sh-stat-status-icon with subtle pulse animation
- Add .sh-stat-overview-detail for contextual information
- Add network gradient color scheme
- Adjust grid for better 5-card layout
This commit is contained in:
CyberMind-FR 2025-12-28 02:47:35 +01:00
parent e7975ecb7a
commit fadf606f31
4 changed files with 148 additions and 15 deletions

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-system-hub
PKG_VERSION:=0.3.1
PKG_VERSION:=0.3.2
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>

View File

@ -65,10 +65,10 @@
font-weight: 700;
}
/* === Stats Overview Grid (Demo Style - Compact) === */
/* === Stats Overview Grid (v0.3.2 - updated for 5 cards) === */
.sh-stats-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
@ -107,6 +107,9 @@
.sh-stat-cpu::before { background: linear-gradient(90deg, #6366f1, #8b5cf6); opacity: 1; }
.sh-stat-memory::before { background: linear-gradient(90deg, #8b5cf6, #ec4899); opacity: 1; }
.sh-stat-disk::before { background: linear-gradient(90deg, #ec4899, #f43f5e); opacity: 1; }
.sh-stat-network::before { background: linear-gradient(90deg, #06b6d4, #3b82f6); opacity: 1; }
.sh-stat-ok::before { background: #22c55e; opacity: 1; }
.sh-stat-error::before { background: #ef4444; opacity: 1; }
.sh-stat-overview-icon {
font-size: 36px;
@ -121,6 +124,10 @@
margin-bottom: 10px;
color: var(--sh-text-primary);
font-family: 'JetBrains Mono', monospace;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.sh-stat-overview-label {
@ -138,6 +145,26 @@
color: var(--sh-text-secondary);
}
/* v0.3.2 - Enhanced dynamic stats */
.sh-stat-status-icon {
font-size: 18px;
opacity: 0.9;
animation: pulse-subtle 2s ease-in-out infinite;
}
.sh-stat-overview-detail {
font-size: 10px;
font-weight: 500;
color: var(--sh-text-muted);
margin-top: 6px;
font-family: 'JetBrains Mono', monospace;
}
@keyframes pulse-subtle {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
/* === Old Header (deprecated, keep for compatibility) === */
.sh-overview-header {
display: flex;

View File

@ -79,7 +79,7 @@ return view.extend({
]),
E('div', { 'class': 'sh-dashboard-header-info' }, [
E('span', { 'class': 'sh-dashboard-badge sh-dashboard-badge-version' },
'v0.3.1'),
'v0.3.2'),
E('span', { 'class': 'sh-dashboard-badge' },
'⏱️ ' + (this.sysInfo.uptime_formatted || '0d 0h 0m')),
E('span', { 'class': 'sh-dashboard-badge' },
@ -94,26 +94,96 @@ return view.extend({
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
var scoreLabel = score >= 80 ? 'Excellent' : (score >= 60 ? 'Good' : (score >= 40 ? 'Warning' : 'Critical'));
// Enhanced stats with status indicators (v0.3.2)
var cpu = this.healthData.cpu || {};
var memory = this.healthData.memory || {};
var disk = this.healthData.disk || {};
var network = this.healthData.network || {};
// Process count (v0.3.2)
var processes = (cpu.processes_running || 0) + '/' + (cpu.processes_total || 0);
// Network throughput (v0.3.2) - format bytes
var rxGB = ((network.rx_bytes || 0) / 1024 / 1024 / 1024).toFixed(2);
var txGB = ((network.tx_bytes || 0) / 1024 / 1024 / 1024).toFixed(2);
// Status icons (v0.3.2)
var getStatusIcon = function(status) {
if (status === 'critical') return '⚠️';
if (status === 'warning') return '⚡';
return '✓';
};
return E('div', { 'class': 'sh-stats-overview-grid' }, [
// Health Score Card
E('div', { 'class': 'sh-stat-overview-card sh-stat-' + scoreClass }, [
E('div', { 'class': 'sh-stat-overview-value' }, score),
E('div', { 'class': 'sh-stat-overview-label' }, 'Health Score'),
E('div', { 'class': 'sh-stat-overview-status' }, scoreLabel)
]),
E('div', { 'class': 'sh-stat-overview-card sh-stat-cpu' }, [
// CPU Card with enhanced info
E('div', {
'class': 'sh-stat-overview-card sh-stat-cpu sh-stat-' + (cpu.status || 'ok'),
'title': 'Load: ' + (cpu.load_1m || '0') + ' | ' + (cpu.cores || 0) + ' cores | ' + processes + ' processes'
}, [
E('div', { 'class': 'sh-stat-overview-icon' }, '🔥'),
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.cpu?.usage || 0) + '%'),
E('div', { 'class': 'sh-stat-overview-label' }, 'CPU Usage')
E('div', { 'class': 'sh-stat-overview-value' }, [
E('span', {}, (cpu.usage || 0) + '%'),
E('span', { 'class': 'sh-stat-status-icon' }, getStatusIcon(cpu.status))
]),
E('div', { 'class': 'sh-stat-overview-label' }, 'CPU Usage'),
E('div', { 'class': 'sh-stat-overview-detail' },
'Load: ' + (cpu.load_1m || '0') + ' • ' + processes + ' proc')
]),
E('div', { 'class': 'sh-stat-overview-card sh-stat-memory' }, [
// Memory Card with swap info
E('div', {
'class': 'sh-stat-overview-card sh-stat-memory sh-stat-' + (memory.status || 'ok'),
'title': ((memory.used_kb || 0) / 1024).toFixed(0) + ' MB / ' + ((memory.total_kb || 0) / 1024).toFixed(0) + ' MB' +
(memory.swap_total_kb > 0 ? ' | Swap: ' + (memory.swap_usage || 0) + '%' : '')
}, [
E('div', { 'class': 'sh-stat-overview-icon' }, '💾'),
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.memory?.usage || 0) + '%'),
E('div', { 'class': 'sh-stat-overview-label' }, 'Memory Usage')
E('div', { 'class': 'sh-stat-overview-value' }, [
E('span', {}, (memory.usage || 0) + '%'),
E('span', { 'class': 'sh-stat-status-icon' }, getStatusIcon(memory.status))
]),
E('div', { 'class': 'sh-stat-overview-label' }, 'Memory'),
E('div', { 'class': 'sh-stat-overview-detail' },
((memory.used_kb || 0) / 1024).toFixed(0) + 'MB / ' +
((memory.total_kb || 0) / 1024).toFixed(0) + 'MB' +
(memory.swap_total_kb > 0 ? ' • Swap: ' + (memory.swap_usage || 0) + '%' : ''))
]),
E('div', { 'class': 'sh-stat-overview-card sh-stat-disk' }, [
// Disk Card
E('div', {
'class': 'sh-stat-overview-card sh-stat-disk sh-stat-' + (disk.status || 'ok'),
'title': ((disk.used_kb || 0) / 1024 / 1024).toFixed(1) + ' GB / ' + ((disk.total_kb || 0) / 1024 / 1024).toFixed(1) + ' GB'
}, [
E('div', { 'class': 'sh-stat-overview-icon' }, '💿'),
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.disk?.usage || 0) + '%'),
E('div', { 'class': 'sh-stat-overview-label' }, 'Disk Usage')
E('div', { 'class': 'sh-stat-overview-value' }, [
E('span', {}, (disk.usage || 0) + '%'),
E('span', { 'class': 'sh-stat-status-icon' }, getStatusIcon(disk.status))
]),
E('div', { 'class': 'sh-stat-overview-label' }, 'Disk Usage'),
E('div', { 'class': 'sh-stat-overview-detail' },
((disk.used_kb || 0) / 1024 / 1024).toFixed(1) + 'GB / ' +
((disk.total_kb || 0) / 1024 / 1024).toFixed(1) + 'GB')
]),
// Network Card (v0.3.2 - NEW)
E('div', {
'class': 'sh-stat-overview-card sh-stat-network sh-stat-' + (network.wan_up ? 'ok' : 'error'),
'title': 'RX: ' + rxGB + ' GB | TX: ' + txGB + ' GB'
}, [
E('div', { 'class': 'sh-stat-overview-icon' }, '🌐'),
E('div', { 'class': 'sh-stat-overview-value' }, [
E('span', {}, network.wan_up ? 'Online' : 'Offline'),
E('span', { 'class': 'sh-stat-status-icon' }, network.wan_up ? '✓' : '✗')
]),
E('div', { 'class': 'sh-stat-overview-label' }, 'Network'),
E('div', { 'class': 'sh-stat-overview-detail' },
'↓ ' + rxGB + 'GB • ↑ ' + txGB + 'GB')
])
]);
},

View File

@ -106,6 +106,10 @@ get_health() {
local load5=$(echo $load | awk '{print $2}')
local load15=$(echo $load | awk '{print $3}')
# Process count (v0.3.2)
local processes_running=$(echo $load | awk '{split($4,a,"/"); print a[1]}')
local processes_total=$(echo $load | awk '{split($4,a,"/"); print a[2]}')
# Calculate CPU usage percentage (load / cores * 100)
local cpu_usage=$(awk -v load="$load1" -v cores="$cpu_cores" 'BEGIN { printf "%.0f", (load / cores) * 100 }')
[ "$cpu_usage" -gt 100 ] && cpu_usage=100
@ -122,9 +126,11 @@ get_health() {
json_add_string "load_5m" "$load5"
json_add_string "load_15m" "$load15"
json_add_int "cores" "$cpu_cores"
json_add_int "processes_running" "${processes_running:-0}"
json_add_int "processes_total" "${processes_total:-0}"
json_close_object
# Memory
# Memory (v0.3.2: added swap support)
local mem_total=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
local mem_free=$(awk '/MemFree/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
local mem_available=$(awk '/MemAvailable/ {print $2}' /proc/meminfo 2>/dev/null || echo $mem_free)
@ -132,6 +138,15 @@ get_health() {
local mem_cached=$(awk '/^Cached/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
local mem_used=$((mem_total - mem_available))
# Swap info (v0.3.2)
local swap_total=$(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
local swap_free=$(awk '/SwapFree/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
local swap_used=$((swap_total - swap_free))
local swap_usage=0
if [ "$swap_total" -gt 0 ]; then
swap_usage=$(( (swap_used * 100) / swap_total ))
fi
local mem_usage=0
if [ "$mem_total" -gt 0 ]; then
mem_usage=$(( (mem_used * 100) / mem_total ))
@ -151,6 +166,9 @@ get_health() {
json_add_int "cached_kb" "$mem_cached"
json_add_int "usage" "$mem_usage"
json_add_string "status" "$mem_status"
json_add_int "swap_total_kb" "$swap_total"
json_add_int "swap_used_kb" "$swap_used"
json_add_int "swap_usage" "$swap_usage"
json_close_object
# Disk (root filesystem)
@ -190,7 +208,7 @@ get_health() {
json_add_string "status" "$temp_status"
json_close_object
# Network (WAN connectivity)
# Network (WAN connectivity + throughput v0.3.2)
local wan_up=0
local wan_status="error"
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
@ -198,9 +216,27 @@ get_health() {
wan_status="ok"
fi
# Network throughput (v0.3.2) - get total RX/TX from all interfaces
local rx_bytes=0
local tx_bytes=0
for iface in /sys/class/net/*; do
[ -d "$iface" ] || continue
local ifname=$(basename "$iface")
# Skip loopback and virtual interfaces
case "$ifname" in
lo|br-*|wlan*-*) continue ;;
esac
local rx=$(cat "$iface/statistics/rx_bytes" 2>/dev/null || echo 0)
local tx=$(cat "$iface/statistics/tx_bytes" 2>/dev/null || echo 0)
rx_bytes=$((rx_bytes + rx))
tx_bytes=$((tx_bytes + tx))
done
json_add_object "network"
json_add_boolean "wan_up" "$wan_up"
json_add_string "status" "$wan_status"
json_add_int "rx_bytes" "$rx_bytes"
json_add_int "tx_bytes" "$tx_bytes"
json_close_object
# Services