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:
parent
c5e22fd08d
commit
5a856e5da2
@ -1,6 +1,6 @@
|
|||||||
# SecuBox UI & Theme History
|
# SecuBox UI & Theme History
|
||||||
|
|
||||||
_Last updated: 2026-02-04_
|
_Last updated: 2026-02-05_
|
||||||
|
|
||||||
1. **Unified Dashboard Refresh (2025-12-20)**
|
1. **Unified Dashboard Refresh (2025-12-20)**
|
||||||
- Dashboard received the "sh-page-header" layout, hero stats, and SecuNav top tabs.
|
- 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.
|
- Eliminated ~1000 lines of duplicate CSS from module nav files.
|
||||||
- Updated modules: `cdn-cache`, `client-guardian`, `crowdsec-dashboard`, `media-flow`, `mqtt-bridge`, `system-hub`.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@ -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).
|
- ~~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).
|
- ~~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).
|
- ~~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
|
## 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).~~
|
- ~~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).~~
|
- ~~Provide a compact variant for nested modules (e.g., CDN Cache, Network Modes).~~
|
||||||
|
|
||||||
3. **Monitoring UX**
|
3. ~~**Monitoring UX**~~ — Done (2026-02-05)
|
||||||
- Add empty-state copy while charts warm up.
|
- ~~Add empty-state copy while charts warm up.~~
|
||||||
- Display bandwidth units dynamically (Kbps/Mbps/Gbps) based on rate.
|
- ~~Display bandwidth units dynamically (Kbps/Mbps/Gbps) based on rate.~~
|
||||||
|
|
||||||
4. **MAC Guardian Feed Integration**
|
4. **MAC Guardian Feed Integration**
|
||||||
- Build and include mac-guardian IPK in bonus feed (new package from 2026-02-03, not yet in feed).
|
- Build and include mac-guardian IPK in bonus feed (new package from 2026-02-03, not yet in feed).
|
||||||
|
|||||||
@ -83,6 +83,14 @@
|
|||||||
- Updated module navs: cdn-cache, client-guardian, crowdsec-dashboard, media-flow, mqtt-bridge, system-hub.
|
- 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.
|
- 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
|
## Next Up
|
||||||
|
|
||||||
1. Rebuild bonus feed with all 2026-02-04/05 changes (IPK files need rebuild).
|
1. Rebuild bonus feed with all 2026-02-04/05 changes (IPK files need rebuild).
|
||||||
|
|||||||
@ -299,6 +299,16 @@ function formatBytes(bytes) {
|
|||||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
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({
|
return baseclass.extend({
|
||||||
getStatus: callStatus,
|
getStatus: callStatus,
|
||||||
getModules: callModules,
|
getModules: callModules,
|
||||||
@ -349,5 +359,6 @@ return baseclass.extend({
|
|||||||
p2pSetSettings: callP2PSetSettings,
|
p2pSetSettings: callP2PSetSettings,
|
||||||
// Utilities
|
// Utilities
|
||||||
formatUptime: formatUptime,
|
formatUptime: formatUptime,
|
||||||
formatBytes: formatBytes
|
formatBytes: formatBytes,
|
||||||
|
formatBits: formatBits
|
||||||
});
|
});
|
||||||
|
|||||||
@ -95,6 +95,61 @@
|
|||||||
overflow: hidden;
|
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 {
|
.secubox-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -238,3 +293,12 @@
|
|||||||
border-color: var(--cyber-accent-primary);
|
border-color: var(--cyber-accent-primary);
|
||||||
box-shadow: 0 0 20px rgba(102, 126, 234, 0.2);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -151,7 +151,16 @@ return view.extend({
|
|||||||
renderChartCard: function(type, title, unit, accent) {
|
renderChartCard: function(type, title, unit, accent) {
|
||||||
return E('div', { 'class': 'secubox-chart-card' }, [
|
return E('div', { 'class': 'secubox-chart-card' }, [
|
||||||
E('h3', { 'class': 'secubox-chart-title' }, title),
|
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', {
|
E('svg', {
|
||||||
'id': 'chart-' + type,
|
'id': 'chart-' + type,
|
||||||
'class': 'secubox-chart',
|
'class': 'secubox-chart',
|
||||||
@ -159,10 +168,10 @@ return view.extend({
|
|||||||
'preserveAspectRatio': 'none',
|
'preserveAspectRatio': 'none',
|
||||||
'data-accent': accent
|
'data-accent': accent
|
||||||
})
|
})
|
||||||
),
|
]),
|
||||||
E('div', { 'class': 'secubox-chart-legend' }, [
|
E('div', { 'class': 'secubox-chart-legend' }, [
|
||||||
E('span', { 'id': 'current-' + type, 'class': 'secubox-current-value' }, '0' + unit),
|
E('span', { 'id': 'current-' + type, 'class': 'secubox-current-value' }, '—'),
|
||||||
E('span', { 'class': 'secubox-chart-unit' }, _('Live'))
|
E('span', { 'id': 'unit-' + type, 'class': 'secubox-chart-unit' }, _('Waiting'))
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@ -214,9 +223,18 @@ return view.extend({
|
|||||||
|
|
||||||
drawChart: function(type, data, color) {
|
drawChart: function(type, data, color) {
|
||||||
var svg = document.getElementById('chart-' + type);
|
var svg = document.getElementById('chart-' + type);
|
||||||
|
var emptyEl = document.getElementById('chart-empty-' + type);
|
||||||
var currentEl = document.getElementById('current-' + 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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide empty state, show chart
|
||||||
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
|
if (unitEl) unitEl.textContent = _('Live');
|
||||||
|
|
||||||
var width = 600;
|
var width = 600;
|
||||||
var height = 200;
|
var height = 200;
|
||||||
@ -294,15 +312,24 @@ return view.extend({
|
|||||||
|
|
||||||
if (currentEl) {
|
if (currentEl) {
|
||||||
var last = rates[rates.length - 1];
|
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() {
|
drawLoadChart: function() {
|
||||||
var svg = document.getElementById('chart-load');
|
var svg = document.getElementById('chart-load');
|
||||||
|
var emptyEl = document.getElementById('chart-empty-load');
|
||||||
var currentEl = document.getElementById('current-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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide empty state, show chart
|
||||||
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
|
if (unitEl) unitEl.textContent = _('Live');
|
||||||
|
|
||||||
var width = 600;
|
var width = 600;
|
||||||
var height = 200;
|
var height = 200;
|
||||||
@ -385,7 +412,7 @@ return view.extend({
|
|||||||
|
|
||||||
getNetworkRateSummary: function() {
|
getNetworkRateSummary: function() {
|
||||||
if (this.networkHistory.length < 2)
|
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 last = this.networkHistory[this.networkHistory.length - 1];
|
||||||
var prev = this.networkHistory[this.networkHistory.length - 2];
|
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);
|
var tx = Math.max(0, (last.tx - prev.tx) / seconds);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
summary: API.formatBytes(rx) + '/s ↓ · ' + API.formatBytes(tx) + '/s ↑'
|
summary: API.formatBits(rx) + ' ↓ · ' + API.formatBits(tx) + ' ↑',
|
||||||
|
rx: rx,
|
||||||
|
tx: tx
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user