From 5a856e5da231ecdb059a3171788dbee090e6b899 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 5 Feb 2026 04:04:32 +0100 Subject: [PATCH] feat(monitoring): Add empty-state loading and dynamic bandwidth units - Add animated "Collecting data..." overlay with pulsing dots during 5-second chart warmup period - Chart legend transitions from "Waiting" to "Live" when data arrives - Add formatBits() helper for network rate display (Kbps/Mbps/Gbps) - Network rates now use SI units (bits) instead of bytes - Cyberpunk theme support for empty state styling Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 12 +++- .claude/TODO.md | 7 +- .claude/WIP.md | 8 +++ .../luci-static/resources/secubox/api.js | 13 +++- .../resources/secubox/monitoring.css | 64 +++++++++++++++++++ .../resources/view/secubox/monitoring.js | 47 +++++++++++--- 6 files changed, 137 insertions(+), 14 deletions(-) diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 926136e1..f8334b32 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1,6 +1,6 @@ # SecuBox UI & Theme History -_Last updated: 2026-02-04_ +_Last updated: 2026-02-05_ 1. **Unified Dashboard Refresh (2025-12-20)** - Dashboard received the "sh-page-header" layout, hero stats, and SecuNav top tabs. @@ -164,3 +164,13 @@ _Last updated: 2026-02-04_ - Eliminated ~1000 lines of duplicate CSS from module nav files. - Updated modules: `cdn-cache`, `client-guardian`, `crowdsec-dashboard`, `media-flow`, `mqtt-bridge`, `system-hub`. - Views no longer need to require Theme separately or manually load CSS. + +21. **Monitoring UX Improvements (2026-02-05)** + - Empty-state loading animation for charts during 5-second data collection warmup. + - Animated "Collecting data..." overlay with pulsing dots. + - Chart legend shows "Waiting" → "Live" transition. + - Cyberpunk theme support for empty state styling. + - Dynamic bandwidth units via new `formatBits()` helper. + - Network rates now display in bits (Kbps/Mbps/Gbps) instead of bytes. + - Uses SI units (1000 base) for industry-standard notation. + - Dash placeholder ("— ↓ · — ↑") before first data point. diff --git a/.claude/TODO.md b/.claude/TODO.md index feeeed36..11e37101 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -12,6 +12,7 @@ _Last updated: 2026-02-05_ - ~~SMB/CIFS Shared Remote Directories~~ — Done: `secubox-app-smbfs` (client mount manager) + `secubox-app-ksmbd` (server for mesh sharing) (2026-02-04/05). - ~~P2P App Store Emancipation~~ — Done: P2P package distribution, packages.js view, devstatus.js widget (2026-02-04/05). - ~~Navigation Component~~ — Done: `SecuNav.renderTabs()` now auto-inits theme+CSS, `renderCompactTabs()` for nested modules (2026-02-05). +- ~~Monitoring UX~~ — Done: Empty-state loading animation for charts, dynamic bandwidth units in bits (Kbps/Mbps/Gbps) via `formatBits()` (2026-02-05). ## Open @@ -23,9 +24,9 @@ _Last updated: 2026-02-05_ - ~~Convert `SecuNav.renderTabs()` into a reusable LuCI widget (avoid duplicating `Theme.init` in each view).~~ - ~~Provide a compact variant for nested modules (e.g., CDN Cache, Network Modes).~~ -3. **Monitoring UX** - - Add empty-state copy while charts warm up. - - Display bandwidth units dynamically (Kbps/Mbps/Gbps) based on rate. +3. ~~**Monitoring UX**~~ — Done (2026-02-05) + - ~~Add empty-state copy while charts warm up.~~ + - ~~Display bandwidth units dynamically (Kbps/Mbps/Gbps) based on rate.~~ 4. **MAC Guardian Feed Integration** - Build and include mac-guardian IPK in bonus feed (new package from 2026-02-03, not yet in feed). diff --git a/.claude/WIP.md b/.claude/WIP.md index 82f30f60..671f4a79 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -83,6 +83,14 @@ - Updated module navs: cdn-cache, client-guardian, crowdsec-dashboard, media-flow, mqtt-bridge, system-hub. - Removed ~1000 lines of duplicate CSS from module nav files. +- **Monitoring UX Improvements** + Status: DONE (2026-02-05) + Notes: Empty-state loading and dynamic bandwidth units. + - Empty-state overlay with animated dots during 5-second warmup. + - Chart legend "Waiting" → "Live" transition. + - `formatBits()` helper for network rates (Kbps/Mbps/Gbps). + - Cyberpunk theme support for empty state. + ## Next Up 1. Rebuild bonus feed with all 2026-02-04/05 changes (IPK files need rebuild). diff --git a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js index a6198dd8..96c8ceb3 100644 --- a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js +++ b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js @@ -299,6 +299,16 @@ function formatBytes(bytes) { return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; } +function formatBits(bytes, decimals) { + if (!bytes) return '0 bps'; + var bits = bytes * 8; + var k = 1000; // SI units (1000, not 1024) + var sizes = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps']; + var i = Math.floor(Math.log(bits) / Math.log(k)); + var d = (decimals !== undefined) ? decimals : 1; + return (bits / Math.pow(k, i)).toFixed(d) + ' ' + sizes[i]; +} + return baseclass.extend({ getStatus: callStatus, getModules: callModules, @@ -349,5 +359,6 @@ return baseclass.extend({ p2pSetSettings: callP2PSetSettings, // Utilities formatUptime: formatUptime, - formatBytes: formatBytes + formatBytes: formatBytes, + formatBits: formatBits }); diff --git a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css index 5bb321cc..a1ee27a6 100644 --- a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css +++ b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/monitoring.css @@ -95,6 +95,61 @@ overflow: hidden; } +/* Empty State / Loading Skeleton */ +.secubox-chart-empty { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--sb-text-muted); + z-index: 1; +} + +.secubox-chart-empty-icon { + font-size: 32px; + opacity: 0.5; + animation: pulse 2s ease-in-out infinite; +} + +.secubox-chart-empty-text { + font-size: 14px; + text-align: center; +} + +.secubox-chart-empty-progress { + display: flex; + gap: 4px; + margin-top: 8px; +} + +.secubox-chart-empty-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--sh-primary); + opacity: 0.3; + animation: dotPulse 1.5s ease-in-out infinite; +} + +.secubox-chart-empty-dot:nth-child(2) { animation-delay: 0.2s; } +.secubox-chart-empty-dot:nth-child(3) { animation-delay: 0.4s; } + +@keyframes pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +@keyframes dotPulse { + 0%, 100% { opacity: 0.3; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.2); } +} + .secubox-chart { width: 100%; height: 100%; @@ -238,3 +293,12 @@ border-color: var(--cyber-accent-primary); box-shadow: 0 0 20px rgba(102, 126, 234, 0.2); } + +[data-secubox-theme="cyberpunk"] .secubox-chart-empty-dot { + background: var(--cyber-accent-primary); + box-shadow: 0 0 8px var(--cyber-accent-primary); +} + +[data-secubox-theme="cyberpunk"] .secubox-chart-empty-icon { + text-shadow: 0 0 10px rgba(102, 126, 234, 0.5); +} diff --git a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js index e20cadc5..b510c861 100644 --- a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js +++ b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js @@ -151,7 +151,16 @@ return view.extend({ renderChartCard: function(type, title, unit, accent) { return E('div', { 'class': 'secubox-chart-card' }, [ E('h3', { 'class': 'secubox-chart-title' }, title), - E('div', { 'class': 'secubox-chart-container' }, + E('div', { 'class': 'secubox-chart-container' }, [ + E('div', { 'id': 'chart-empty-' + type, 'class': 'secubox-chart-empty' }, [ + E('span', { 'class': 'secubox-chart-empty-icon' }, '📊'), + E('span', { 'class': 'secubox-chart-empty-text' }, _('Collecting data...')), + E('div', { 'class': 'secubox-chart-empty-progress' }, [ + E('span', { 'class': 'secubox-chart-empty-dot' }), + E('span', { 'class': 'secubox-chart-empty-dot' }), + E('span', { 'class': 'secubox-chart-empty-dot' }) + ]) + ]), E('svg', { 'id': 'chart-' + type, 'class': 'secubox-chart', @@ -159,10 +168,10 @@ return view.extend({ 'preserveAspectRatio': 'none', 'data-accent': accent }) - ), + ]), E('div', { 'class': 'secubox-chart-legend' }, [ - E('span', { 'id': 'current-' + type, 'class': 'secubox-current-value' }, '0' + unit), - E('span', { 'class': 'secubox-chart-unit' }, _('Live')) + E('span', { 'id': 'current-' + type, 'class': 'secubox-current-value' }, '—'), + E('span', { 'id': 'unit-' + type, 'class': 'secubox-chart-unit' }, _('Waiting')) ]) ]); }, @@ -214,9 +223,18 @@ return view.extend({ drawChart: function(type, data, color) { var svg = document.getElementById('chart-' + type); + var emptyEl = document.getElementById('chart-empty-' + type); var currentEl = document.getElementById('current-' + type); - if (!svg || data.length === 0) + var unitEl = document.getElementById('unit-' + type); + + if (!svg || data.length === 0) { + if (emptyEl) emptyEl.style.display = 'flex'; return; + } + + // Hide empty state, show chart + if (emptyEl) emptyEl.style.display = 'none'; + if (unitEl) unitEl.textContent = _('Live'); var width = 600; var height = 200; @@ -294,15 +312,24 @@ return view.extend({ if (currentEl) { var last = rates[rates.length - 1]; - currentEl.textContent = API.formatBytes(last.rx + last.tx) + '/s'; + currentEl.textContent = API.formatBits(last.rx + last.tx); } }, drawLoadChart: function() { var svg = document.getElementById('chart-load'); + var emptyEl = document.getElementById('chart-empty-load'); var currentEl = document.getElementById('current-load'); - if (!svg || this.loadHistory.length === 0) + var unitEl = document.getElementById('unit-load'); + + if (!svg || this.loadHistory.length === 0) { + if (emptyEl) emptyEl.style.display = 'flex'; return; + } + + // Hide empty state, show chart + if (emptyEl) emptyEl.style.display = 'none'; + if (unitEl) unitEl.textContent = _('Live'); var width = 600; var height = 200; @@ -385,7 +412,7 @@ return view.extend({ getNetworkRateSummary: function() { if (this.networkHistory.length < 2) - return { summary: '0 B/s' }; + return { summary: '— ↓ · — ↑', rx: 0, tx: 0 }; var last = this.networkHistory[this.networkHistory.length - 1]; var prev = this.networkHistory[this.networkHistory.length - 2]; @@ -394,7 +421,9 @@ return view.extend({ var tx = Math.max(0, (last.tx - prev.tx) / seconds); return { - summary: API.formatBytes(rx) + '/s ↓ · ' + API.formatBytes(tx) + '/s ↑' + summary: API.formatBits(rx) + ' ↓ · ' + API.formatBits(tx) + ' ↑', + rx: rx, + tx: tx }; },