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
|
||||
|
||||
_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.
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user