secubox-openwrt/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js
CyberMind-FR f86b67ce13 fix(media-flow): Fix dashboard theming and flow count display
- Add SecuBox dark theme initialization to all views (dashboard, alerts,
  clients, services, history)
- Fix flow count detection by using jsonfilter instead of jq (OpenWrt native)
- Prioritize /var/run/netifyd/status.json for ndpid-compat flow data
- Remove filtering expect{} from API.getActiveStreams() RPC declaration
- Update CLAUDE.md with jsonfilter usage guidelines for OpenWrt

The dashboard now correctly displays:
- Total Flows count from nDPId via ndpid-compat
- nDPId/Netifyd status indicators
- SecuBox dark theme with portal header

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 07:31:21 +01:00

291 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require poll';
'require ui';
'require media-flow/api as API';
'require media-flow/nav as NavHelper';
'require secubox-portal/header as SbHeader';
return view.extend({
title: _('Media Flow Dashboard'),
pollInterval: 5,
// Initialize SecuBox dark theme
initTheme: function() {
// Set dark theme on document
document.documentElement.setAttribute('data-theme', 'dark');
document.body.classList.add('secubox-mode');
// Apply dark background to body for SecuBox styling
if (!document.getElementById('mf-theme-styles')) {
var themeStyle = document.createElement('style');
themeStyle.id = 'mf-theme-styles';
themeStyle.textContent = `
body.secubox-mode { background: #0a0a0f !important; }
body.secubox-mode .main-right,
body.secubox-mode #maincontent,
body.secubox-mode .container { background: transparent !important; }
`;
document.head.appendChild(themeStyle);
}
},
formatBytes: function(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
},
load: function() {
return Promise.all([
API.getStatus(),
API.getActiveStreams(),
API.getStatsByService()
]);
},
render: function(data) {
var self = this;
// Initialize SecuBox dark theme
this.initTheme();
var status = data[0] || {};
var streamsData = data[1] || {};
var statsByService = data[2] || {};
var dpiSource = status.dpi_source || 'none';
var isNdpid = dpiSource === 'ndpid';
var isNetifyd = dpiSource === 'netifyd';
var streams = streamsData.streams || [];
var flowCount = streamsData.flow_count || status.active_flows || status.ndpid_flows || 0;
// Inject CSS
var css = `
.mf-dashboard { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #e4e4e7; }
.mf-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, rgba(236, 72, 153, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%); border-radius: 16px; border: 1px solid rgba(236, 72, 153, 0.3); }
.mf-logo { display: flex; align-items: center; gap: 12px; }
.mf-logo-icon { width: 48px; height: 48px; background: linear-gradient(135deg, #ec4899, #8b5cf6); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.mf-logo-text { font-size: 1.5rem; font-weight: 700; background: linear-gradient(135deg, #ec4899, #8b5cf6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.mf-status { display: flex; align-items: center; gap: 16px; }
.mf-status-badge { padding: 8px 16px; border-radius: 20px; font-size: 0.875rem; font-weight: 500; display: flex; align-items: center; gap: 8px; }
.mf-status-badge.running { background: rgba(34, 197, 94, 0.2); color: #22c55e; border: 1px solid rgba(34, 197, 94, 0.3); }
.mf-status-badge.stopped { background: rgba(239, 68, 68, 0.2); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); }
.mf-status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.mf-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
@media (max-width: 768px) { .mf-stats-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .mf-stats-grid { grid-template-columns: 1fr; } }
.mf-stat-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px; text-align: center; transition: all 0.2s; }
.mf-stat-card:hover { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.12); }
.mf-stat-icon { font-size: 1.5rem; margin-bottom: 8px; }
.mf-stat-value { font-size: 2rem; font-weight: 700; margin-bottom: 4px; }
.mf-stat-value.cyan { color: #06b6d4; }
.mf-stat-value.pink { color: #ec4899; }
.mf-stat-value.green { color: #22c55e; }
.mf-stat-value.yellow { color: #fbbf24; }
.mf-stat-label { font-size: 0.875rem; color: #a1a1aa; }
.mf-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; margin-bottom: 24px; overflow: hidden; }
.mf-card-header { padding: 16px 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); display: flex; align-items: center; justify-content: space-between; }
.mf-card-title { font-size: 1rem; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.mf-card-badge { font-size: 0.75rem; padding: 4px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 12px; color: #a1a1aa; }
.mf-card-body { padding: 20px; }
.mf-notice { padding: 16px 20px; border-radius: 12px; margin-bottom: 24px; display: flex; align-items: center; gap: 12px; }
.mf-notice.success { background: rgba(34, 197, 94, 0.15); border: 1px solid rgba(34, 197, 94, 0.3); color: #e4e4e7; }
.mf-notice.warning { background: rgba(251, 191, 36, 0.15); border: 1px solid rgba(251, 191, 36, 0.3); color: #e4e4e7; }
.mf-notice.error { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); color: #e4e4e7; }
.mf-notice-icon { font-size: 1.25rem; }
.mf-notice-text strong { color: #22c55e; }
.mf-notice.warning .mf-notice-text strong { color: #fbbf24; }
.mf-notice.error .mf-notice-text strong { color: #ef4444; }
.mf-empty { text-align: center; padding: 48px 20px; color: #71717a; }
.mf-empty-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.5; }
.mf-empty-text { font-size: 1rem; }
.mf-streams-table { width: 100%; border-collapse: collapse; overflow-x: auto; }
@media (max-width: 768px) { .mf-streams-table { font-size: 0.85rem; } .mf-streams-table th, .mf-streams-table td { padding: 10px 8px; } }
@media (max-width: 480px) { .mf-card-body { padding: 12px; overflow-x: auto; } .mf-streams-table { font-size: 0.75rem; } .mf-streams-table th, .mf-streams-table td { padding: 8px 4px; } }
.mf-streams-table th { text-align: left; padding: 12px 16px; font-size: 0.75rem; text-transform: uppercase; color: #71717a; border-bottom: 1px solid rgba(255, 255, 255, 0.08); }
.mf-streams-table td { padding: 12px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.mf-streams-table tr:hover td { background: rgba(255, 255, 255, 0.03); }
.mf-quality-badge { padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 600; color: white; }
.mf-btn { padding: 10px 20px; border-radius: 8px; font-size: 0.875rem; font-weight: 500; cursor: pointer; border: none; transition: all 0.2s; }
.mf-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.mf-btn.spinning::after { content: ''; animation: spin 1s linear infinite; display: inline-block; margin-left: 8px; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.mf-loading { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
.mf-btn-primary { background: linear-gradient(135deg, #ec4899, #8b5cf6); color: white; }
.mf-btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
.mf-btn-secondary { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; border: 1px solid rgba(255, 255, 255, 0.2); }
.mf-btn-secondary:hover { background: rgba(255, 255, 255, 0.15); }
.mf-controls { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
`;
var view = E('div', { 'class': 'mf-dashboard' }, [
E('style', {}, css),
E('link', { 'rel': 'stylesheet', 'href': L.resource('media-flow/common.css') }),
NavHelper.renderTabs('dashboard'),
// Header
E('div', { 'class': 'mf-header' }, [
E('div', { 'class': 'mf-logo' }, [
E('div', { 'class': 'mf-logo-icon' }, '🎬'),
E('span', { 'class': 'mf-logo-text' }, 'Media Flow')
]),
E('div', { 'class': 'mf-status' }, [
E('div', { 'class': 'mf-status-badge ' + (isNdpid || isNetifyd ? 'running' : 'stopped') }, [
E('span', { 'class': 'mf-status-dot' }),
isNdpid ? 'nDPId Active' : (isNetifyd ? 'Netifyd Active' : 'No DPI')
])
])
]),
// Stats Grid
E('div', { 'class': 'mf-stats-grid' }, [
E('div', { 'class': 'mf-stat-card' }, [
E('div', { 'class': 'mf-stat-icon' }, '📊'),
E('div', { 'class': 'mf-stat-value cyan', 'id': 'mf-total-flows' }, String(flowCount)),
E('div', { 'class': 'mf-stat-label' }, 'Total Flows')
]),
E('div', { 'class': 'mf-stat-card' }, [
E('div', { 'class': 'mf-stat-icon' }, '🎬'),
E('div', { 'class': 'mf-stat-value pink', 'id': 'mf-stream-count' }, String(streams.length)),
E('div', { 'class': 'mf-stat-label' }, 'Active Streams')
]),
E('div', { 'class': 'mf-stat-card' }, [
E('div', { 'class': 'mf-stat-icon' }, '🔍'),
E('div', { 'class': 'mf-stat-value green' }, status.ndpid_running ? '' : ''),
E('div', { 'class': 'mf-stat-label' }, 'nDPId')
]),
E('div', { 'class': 'mf-stat-card' }, [
E('div', { 'class': 'mf-stat-icon' }, '📡'),
E('div', { 'class': 'mf-stat-value yellow' }, status.netifyd_running ? '' : ''),
E('div', { 'class': 'mf-stat-label' }, 'Netifyd')
])
]),
// DPI Notice
isNdpid ? E('div', { 'class': 'mf-notice success' }, [
E('span', { 'class': 'mf-notice-icon' }, ''),
E('span', { 'class': 'mf-notice-text' }, [
E('strong', {}, 'nDPId Active: '),
'Using local deep packet inspection. No cloud subscription required.'
])
]) : (isNetifyd ? E('div', { 'class': 'mf-notice warning' }, [
E('span', { 'class': 'mf-notice-icon' }, ''),
E('span', { 'class': 'mf-notice-text' }, [
E('strong', {}, 'Netifyd Active: '),
'Cloud subscription may be required for full app detection.'
])
]) : E('div', { 'class': 'mf-notice error' }, [
E('span', { 'class': 'mf-notice-icon' }, ''),
E('span', { 'class': 'mf-notice-text' }, [
E('strong', {}, 'No DPI Engine: '),
'Start nDPId or Netifyd for streaming detection.'
]),
E('div', { 'class': 'mf-controls' }, [
E('button', {
'class': 'mf-btn mf-btn-primary',
'click': function() {
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting nDPId...'))]);
API.startNdpid().then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, 'nDPId started'), 'success');
setTimeout(function() { window.location.reload(); }, 2000);
} else {
ui.addNotification(null, E('p', {}, res.message || 'Failed'), 'error');
}
});
}
}, 'Start nDPId'),
E('button', {
'class': 'mf-btn mf-btn-secondary',
'click': function() {
ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting Netifyd...'))]);
API.startNetifyd().then(function(res) {
ui.hideModal();
if (res && res.success) {
ui.addNotification(null, E('p', {}, 'Netifyd started'), 'success');
setTimeout(function() { window.location.reload(); }, 2000);
} else {
ui.addNotification(null, E('p', {}, res.message || 'Failed'), 'error');
}
});
}
}, 'Start Netifyd')
])
])),
// Active Streams Card
E('div', { 'class': 'mf-card' }, [
E('div', { 'class': 'mf-card-header' }, [
E('div', { 'class': 'mf-card-title' }, [
E('span', {}, '🎬'),
'Active Streams'
]),
E('div', { 'class': 'mf-card-badge', 'id': 'mf-streams-badge' }, streams.length + ' streaming')
]),
E('div', { 'class': 'mf-card-body', 'id': 'mf-streams-container' },
streams.length > 0 ?
E('table', { 'class': 'mf-streams-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', {}, 'Service'),
E('th', {}, 'Client'),
E('th', {}, 'Quality'),
E('th', {}, 'Data')
])
]),
E('tbody', {},
streams.slice(0, 15).map(function(s) {
var qualityColors = { '4K': '#9333ea', 'FHD': '#2563eb', 'HD': '#059669', 'SD': '#d97706' };
var totalBytes = (s.bytes_rx || 0) + (s.bytes_tx || 0);
return E('tr', {}, [
E('td', {}, E('strong', {}, s.app || 'Unknown')),
E('td', {}, s.client || s.src_ip || '-'),
E('td', {}, s.quality ? E('span', { 'class': 'mf-quality-badge', 'style': 'background:' + (qualityColors[s.quality] || '#6b7280') }, s.quality) : '-'),
E('td', {}, self.formatBytes(totalBytes))
]);
})
)
]) :
E('div', { 'class': 'mf-empty' }, [
E('div', { 'class': 'mf-empty-icon' }, '📺'),
E('div', { 'class': 'mf-empty-text' }, isNdpid ? 'No streaming activity detected' : 'Waiting for streaming data...')
])
)
])
]);
// Start polling
poll.add(L.bind(function() {
return API.getActiveStreams().then(L.bind(function(data) {
var el = document.getElementById('mf-total-flows');
if (el) el.textContent = String(data.flow_count || 0);
var el2 = document.getElementById('mf-stream-count');
if (el2) el2.textContent = String((data.streams || []).length);
var badge = document.getElementById('mf-streams-badge');
if (badge) badge.textContent = (data.streams || []).length + ' streaming';
}, this));
}, this), this.pollInterval);
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
wrapper.appendChild(SbHeader.render());
wrapper.appendChild(view);
return wrapper;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});