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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-05 04:04:32 +01:00
parent c5e22fd08d
commit 5a856e5da2
6 changed files with 137 additions and 14 deletions

View File

@ -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.

View File

@ -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).

View File

@ -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).

View File

@ -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
});

View File

@ -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);
}

View File

@ -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
};
},