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:
parent
e7975ecb7a
commit
fadf606f31
@ -1,7 +1,7 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-system-hub
|
PKG_NAME:=luci-app-system-hub
|
||||||
PKG_VERSION:=0.3.1
|
PKG_VERSION:=0.3.2
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|||||||
@ -65,10 +65,10 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Stats Overview Grid (Demo Style - Compact) === */
|
/* === Stats Overview Grid (v0.3.2 - updated for 5 cards) === */
|
||||||
.sh-stats-overview-grid {
|
.sh-stats-overview-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
@ -107,6 +107,9 @@
|
|||||||
.sh-stat-cpu::before { background: linear-gradient(90deg, #6366f1, #8b5cf6); opacity: 1; }
|
.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-memory::before { background: linear-gradient(90deg, #8b5cf6, #ec4899); opacity: 1; }
|
||||||
.sh-stat-disk::before { background: linear-gradient(90deg, #ec4899, #f43f5e); 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 {
|
.sh-stat-overview-icon {
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
@ -121,6 +124,10 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: var(--sh-text-primary);
|
color: var(--sh-text-primary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sh-stat-overview-label {
|
.sh-stat-overview-label {
|
||||||
@ -138,6 +145,26 @@
|
|||||||
color: var(--sh-text-secondary);
|
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) === */
|
/* === Old Header (deprecated, keep for compatibility) === */
|
||||||
.sh-overview-header {
|
.sh-overview-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -79,7 +79,7 @@ return view.extend({
|
|||||||
]),
|
]),
|
||||||
E('div', { 'class': 'sh-dashboard-header-info' }, [
|
E('div', { 'class': 'sh-dashboard-header-info' }, [
|
||||||
E('span', { 'class': 'sh-dashboard-badge sh-dashboard-badge-version' },
|
E('span', { 'class': 'sh-dashboard-badge sh-dashboard-badge-version' },
|
||||||
'v0.3.1'),
|
'v0.3.2'),
|
||||||
E('span', { 'class': 'sh-dashboard-badge' },
|
E('span', { 'class': 'sh-dashboard-badge' },
|
||||||
'⏱️ ' + (this.sysInfo.uptime_formatted || '0d 0h 0m')),
|
'⏱️ ' + (this.sysInfo.uptime_formatted || '0d 0h 0m')),
|
||||||
E('span', { 'class': 'sh-dashboard-badge' },
|
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 scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
|
||||||
var scoreLabel = 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' }, [
|
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-card sh-stat-' + scoreClass }, [
|
||||||
E('div', { 'class': 'sh-stat-overview-value' }, score),
|
E('div', { 'class': 'sh-stat-overview-value' }, score),
|
||||||
E('div', { 'class': 'sh-stat-overview-label' }, 'Health Score'),
|
E('div', { 'class': 'sh-stat-overview-label' }, 'Health Score'),
|
||||||
E('div', { 'class': 'sh-stat-overview-status' }, scoreLabel)
|
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-icon' }, '🔥'),
|
||||||
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.cpu?.usage || 0) + '%'),
|
E('div', { 'class': 'sh-stat-overview-value' }, [
|
||||||
E('div', { 'class': 'sh-stat-overview-label' }, 'CPU Usage')
|
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-icon' }, '💾'),
|
||||||
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.memory?.usage || 0) + '%'),
|
E('div', { 'class': 'sh-stat-overview-value' }, [
|
||||||
E('div', { 'class': 'sh-stat-overview-label' }, 'Memory Usage')
|
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-icon' }, '💿'),
|
||||||
E('div', { 'class': 'sh-stat-overview-value' }, (this.healthData.disk?.usage || 0) + '%'),
|
E('div', { 'class': 'sh-stat-overview-value' }, [
|
||||||
E('div', { 'class': 'sh-stat-overview-label' }, 'Disk Usage')
|
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')
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -106,6 +106,10 @@ get_health() {
|
|||||||
local load5=$(echo $load | awk '{print $2}')
|
local load5=$(echo $load | awk '{print $2}')
|
||||||
local load15=$(echo $load | awk '{print $3}')
|
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)
|
# Calculate CPU usage percentage (load / cores * 100)
|
||||||
local cpu_usage=$(awk -v load="$load1" -v cores="$cpu_cores" 'BEGIN { printf "%.0f", (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
|
[ "$cpu_usage" -gt 100 ] && cpu_usage=100
|
||||||
@ -122,9 +126,11 @@ get_health() {
|
|||||||
json_add_string "load_5m" "$load5"
|
json_add_string "load_5m" "$load5"
|
||||||
json_add_string "load_15m" "$load15"
|
json_add_string "load_15m" "$load15"
|
||||||
json_add_int "cores" "$cpu_cores"
|
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
|
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_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_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)
|
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_cached=$(awk '/^Cached/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
|
||||||
local mem_used=$((mem_total - mem_available))
|
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
|
local mem_usage=0
|
||||||
if [ "$mem_total" -gt 0 ]; then
|
if [ "$mem_total" -gt 0 ]; then
|
||||||
mem_usage=$(( (mem_used * 100) / mem_total ))
|
mem_usage=$(( (mem_used * 100) / mem_total ))
|
||||||
@ -151,6 +166,9 @@ get_health() {
|
|||||||
json_add_int "cached_kb" "$mem_cached"
|
json_add_int "cached_kb" "$mem_cached"
|
||||||
json_add_int "usage" "$mem_usage"
|
json_add_int "usage" "$mem_usage"
|
||||||
json_add_string "status" "$mem_status"
|
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
|
json_close_object
|
||||||
|
|
||||||
# Disk (root filesystem)
|
# Disk (root filesystem)
|
||||||
@ -190,7 +208,7 @@ get_health() {
|
|||||||
json_add_string "status" "$temp_status"
|
json_add_string "status" "$temp_status"
|
||||||
json_close_object
|
json_close_object
|
||||||
|
|
||||||
# Network (WAN connectivity)
|
# Network (WAN connectivity + throughput v0.3.2)
|
||||||
local wan_up=0
|
local wan_up=0
|
||||||
local wan_status="error"
|
local wan_status="error"
|
||||||
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
|
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
|
||||||
@ -198,9 +216,27 @@ get_health() {
|
|||||||
wan_status="ok"
|
wan_status="ok"
|
||||||
fi
|
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_object "network"
|
||||||
json_add_boolean "wan_up" "$wan_up"
|
json_add_boolean "wan_up" "$wan_up"
|
||||||
json_add_string "status" "$wan_status"
|
json_add_string "status" "$wan_status"
|
||||||
|
json_add_int "rx_bytes" "$rx_bytes"
|
||||||
|
json_add_int "tx_bytes" "$tx_bytes"
|
||||||
json_close_object
|
json_close_object
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user