feat: Add SecuBox portal header to Media Flow views
Add portal header and dark theme styling to all Media Flow subviews: - clients.js: Client statistics with portal header - services.js: Service statistics with portal header - history.js: Stream history with portal header - alerts.js: Streaming alerts with portal header Each view now includes: - SecuBox global header with Hub, Admin, Security, Network, Monitoring, System navigation - Internal Media Flow navigation (Dashboard, Clients, Services, History, Alerts) - Consistent dark theme styling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
48da7d71ad
commit
3e86952d48
@ -3,6 +3,32 @@
|
||||
'require form';
|
||||
'require ui';
|
||||
'require media-flow/api as API';
|
||||
'require secubox-portal/header as SbHeader';
|
||||
|
||||
var MEDIAFLOW_NAV = [
|
||||
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||
{ id: 'clients', icon: '👥', label: 'Clients' },
|
||||
{ id: 'services', icon: '🎬', label: 'Services' },
|
||||
{ id: 'history', icon: '📜', label: 'History' },
|
||||
{ id: 'alerts', icon: '🔔', label: 'Alerts' }
|
||||
];
|
||||
|
||||
function renderMediaFlowNav(activeId) {
|
||||
return E('div', {
|
||||
'class': 'sb-app-nav',
|
||||
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;flex-wrap:wrap;'
|
||||
}, MEDIAFLOW_NAV.map(function(item) {
|
||||
var isActive = activeId === item.id;
|
||||
return E('a', {
|
||||
'href': L.url('admin', 'secubox', 'mediaflow', item.id),
|
||||
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||
(isActive ? 'background:linear-gradient(135deg,#ec4899,#8b5cf6);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||
}, [
|
||||
E('span', {}, item.icon),
|
||||
E('span', {}, _(item.label))
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
@ -14,10 +40,9 @@ return L.view.extend({
|
||||
render: function(data) {
|
||||
var alerts = data[0] || [];
|
||||
|
||||
var m = new form.Map('media_flow', _('Streaming Alerts'),
|
||||
_('Configure alerts based on streaming service usage'));
|
||||
var m = new form.Map('media_flow', null, null);
|
||||
|
||||
var s = m.section(form.TypedSection, 'alert', _('Alerts'));
|
||||
var s = m.section(form.TypedSection, 'alert', _('Streaming Alerts'));
|
||||
s.anonymous = false;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
@ -44,32 +69,27 @@ return L.view.extend({
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = o.enabled;
|
||||
|
||||
// Custom add button handler
|
||||
s.addModalOptions = function(s, section_id, ev) {
|
||||
var serviceName = this.section.getUIElement(section_id, 'service');
|
||||
var thresholdInput = this.section.getUIElement(section_id, 'threshold_hours');
|
||||
var actionInput = this.section.getUIElement(section_id, 'action');
|
||||
|
||||
if (serviceName && thresholdInput && actionInput) {
|
||||
var service = serviceName.getValue();
|
||||
var threshold = parseInt(thresholdInput.getValue());
|
||||
var action = actionInput.getValue();
|
||||
|
||||
if (service && threshold) {
|
||||
API.setAlert(service, threshold, action).then(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', _('Alert created successfully')), 'info');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return m.render().then(function(rendered) {
|
||||
return E('div', {}, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
var css = `
|
||||
.mf-page { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #e4e4e7; }
|
||||
.mf-header { margin-bottom: 24px; }
|
||||
.mf-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; }
|
||||
.mf-subtitle { color: #a1a1aa; font-size: 0.875rem; }
|
||||
`;
|
||||
var container = E('div', { 'class': 'mf-page' }, [
|
||||
E('style', {}, css),
|
||||
renderMediaFlowNav('alerts'),
|
||||
E('div', { 'class': 'mf-header' }, [
|
||||
E('div', { 'class': 'mf-title' }, ['🔔 ', _('Streaming Alerts')]),
|
||||
E('div', { 'class': 'mf-subtitle' }, _('Configure alerts based on streaming service usage'))
|
||||
]),
|
||||
rendered
|
||||
]);
|
||||
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
wrapper.appendChild(container);
|
||||
return wrapper;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,6 +2,32 @@
|
||||
'require view';
|
||||
'require ui';
|
||||
'require media-flow/api as API';
|
||||
'require secubox-portal/header as SbHeader';
|
||||
|
||||
var MEDIAFLOW_NAV = [
|
||||
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||
{ id: 'clients', icon: '👥', label: 'Clients' },
|
||||
{ id: 'services', icon: '🎬', label: 'Services' },
|
||||
{ id: 'history', icon: '📜', label: 'History' },
|
||||
{ id: 'alerts', icon: '🔔', label: 'Alerts' }
|
||||
];
|
||||
|
||||
function renderMediaFlowNav(activeId) {
|
||||
return E('div', {
|
||||
'class': 'sb-app-nav',
|
||||
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;flex-wrap:wrap;'
|
||||
}, MEDIAFLOW_NAV.map(function(item) {
|
||||
var isActive = activeId === item.id;
|
||||
return E('a', {
|
||||
'href': L.url('admin', 'secubox', 'mediaflow', item.id),
|
||||
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||
(isActive ? 'background:linear-gradient(135deg,#ec4899,#8b5cf6);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||
}, [
|
||||
E('span', {}, item.icon),
|
||||
E('span', {}, _(item.label))
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
@ -14,55 +40,76 @@ return L.view.extend({
|
||||
var statsByClient = data[0] || {};
|
||||
var clients = statsByClient.clients || {};
|
||||
|
||||
var v = E('div', { 'class': 'cbi-map' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
E('h2', {}, _('Clients Statistics')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Streaming activity per client'))
|
||||
var css = `
|
||||
.mf-page { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #e4e4e7; }
|
||||
.mf-header { margin-bottom: 24px; }
|
||||
.mf-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; }
|
||||
.mf-subtitle { color: #a1a1aa; font-size: 0.875rem; }
|
||||
.mf-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; overflow: hidden; }
|
||||
.mf-table { width: 100%; border-collapse: collapse; }
|
||||
.mf-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); background: rgba(255,255,255,0.02); }
|
||||
.mf-table td { padding: 12px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
|
||||
.mf-table tr:hover td { background: rgba(255, 255, 255, 0.03); }
|
||||
.mf-empty { text-align: center; padding: 48px 20px; color: #71717a; }
|
||||
.mf-empty-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.5; }
|
||||
`;
|
||||
|
||||
var container = E('div', { 'class': 'mf-page' }, [
|
||||
E('style', {}, css),
|
||||
renderMediaFlowNav('clients'),
|
||||
E('div', { 'class': 'mf-header' }, [
|
||||
E('div', { 'class': 'mf-title' }, ['👥 ', _('Clients Statistics')]),
|
||||
E('div', { 'class': 'mf-subtitle' }, _('Streaming activity per client'))
|
||||
])
|
||||
]);
|
||||
|
||||
var clientsList = Object.keys(clients);
|
||||
|
||||
if (clientsList.length === 0) {
|
||||
v.appendChild(E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', { 'style': 'font-style: italic; text-align: center; padding: 20px' },
|
||||
_('No client data available yet'))
|
||||
container.appendChild(E('div', { 'class': 'mf-card' }, [
|
||||
E('div', { 'class': 'mf-empty' }, [
|
||||
E('div', { 'class': 'mf-empty-icon' }, '👥'),
|
||||
E('div', {}, _('No client data available yet'))
|
||||
])
|
||||
]));
|
||||
return v;
|
||||
} else {
|
||||
clientsList.sort(function(a, b) {
|
||||
return (clients[b].total_duration_seconds || 0) - (clients[a].total_duration_seconds || 0);
|
||||
});
|
||||
|
||||
var table = E('table', { 'class': 'mf-table' }, [
|
||||
E('thead', {}, [
|
||||
E('tr', {}, [
|
||||
E('th', {}, _('Client IP')),
|
||||
E('th', {}, _('Sessions')),
|
||||
E('th', {}, _('Total Duration')),
|
||||
E('th', {}, _('Total Bandwidth')),
|
||||
E('th', {}, _('Top Service'))
|
||||
])
|
||||
]),
|
||||
E('tbody', {}, clientsList.map(function(client) {
|
||||
var stats = clients[client];
|
||||
var duration = stats.total_duration_seconds || 0;
|
||||
var hours = Math.floor(duration / 3600);
|
||||
var minutes = Math.floor((duration % 3600) / 60);
|
||||
|
||||
return E('tr', {}, [
|
||||
E('td', {}, E('strong', {}, client)),
|
||||
E('td', {}, String(stats.sessions)),
|
||||
E('td', {}, hours + 'h ' + minutes + 'm'),
|
||||
E('td', {}, Math.round(stats.total_bandwidth_kbps) + ' kbps'),
|
||||
E('td', {}, stats.top_service || 'N/A')
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
|
||||
container.appendChild(E('div', { 'class': 'mf-card' }, table));
|
||||
}
|
||||
|
||||
// Sort by total duration
|
||||
clientsList.sort(function(a, b) {
|
||||
return (clients[b].total_duration_seconds || 0) - (clients[a].total_duration_seconds || 0);
|
||||
});
|
||||
|
||||
var table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Client IP')),
|
||||
E('th', { 'class': 'th' }, _('Sessions')),
|
||||
E('th', { 'class': 'th' }, _('Total Duration')),
|
||||
E('th', { 'class': 'th' }, _('Total Bandwidth')),
|
||||
E('th', { 'class': 'th' }, _('Top Service'))
|
||||
])
|
||||
]);
|
||||
|
||||
clientsList.forEach(function(client) {
|
||||
var stats = clients[client];
|
||||
var duration = stats.total_duration_seconds || 0;
|
||||
var hours = Math.floor(duration / 3600);
|
||||
var minutes = Math.floor((duration % 3600) / 60);
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, client),
|
||||
E('td', { 'class': 'td' }, String(stats.sessions)),
|
||||
E('td', { 'class': 'td' }, hours + 'h ' + minutes + 'm'),
|
||||
E('td', { 'class': 'td' }, Math.round(stats.total_bandwidth_kbps) + ' kbps'),
|
||||
E('td', { 'class': 'td' }, stats.top_service || 'N/A')
|
||||
]));
|
||||
});
|
||||
|
||||
v.appendChild(E('div', { 'class': 'cbi-section' }, table));
|
||||
|
||||
return v;
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
wrapper.appendChild(container);
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
@ -2,6 +2,32 @@
|
||||
'require view';
|
||||
'require ui';
|
||||
'require media-flow/api as API';
|
||||
'require secubox-portal/header as SbHeader';
|
||||
|
||||
var MEDIAFLOW_NAV = [
|
||||
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||
{ id: 'clients', icon: '👥', label: 'Clients' },
|
||||
{ id: 'services', icon: '🎬', label: 'Services' },
|
||||
{ id: 'history', icon: '📜', label: 'History' },
|
||||
{ id: 'alerts', icon: '🔔', label: 'Alerts' }
|
||||
];
|
||||
|
||||
function renderMediaFlowNav(activeId) {
|
||||
return E('div', {
|
||||
'class': 'sb-app-nav',
|
||||
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;flex-wrap:wrap;'
|
||||
}, MEDIAFLOW_NAV.map(function(item) {
|
||||
var isActive = activeId === item.id;
|
||||
return E('a', {
|
||||
'href': L.url('admin', 'secubox', 'mediaflow', item.id),
|
||||
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||
(isActive ? 'background:linear-gradient(135deg,#ec4899,#8b5cf6);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||
}, [
|
||||
E('span', {}, item.icon),
|
||||
E('span', {}, _(item.label))
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
@ -14,119 +40,128 @@ return L.view.extend({
|
||||
var historyData = data[0] || {};
|
||||
var history = historyData.history || [];
|
||||
|
||||
var v = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('Stream History')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Historical record of detected streaming sessions'))
|
||||
]);
|
||||
var css = `
|
||||
.mf-page { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #e4e4e7; }
|
||||
.mf-header { margin-bottom: 24px; }
|
||||
.mf-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; }
|
||||
.mf-subtitle { color: #a1a1aa; font-size: 0.875rem; }
|
||||
.mf-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; overflow: hidden; }
|
||||
.mf-controls { display: flex; gap: 10px; align-items: center; margin-bottom: 20px; flex-wrap: wrap; }
|
||||
.mf-select { padding: 8px 12px; border-radius: 6px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: #e4e4e7; }
|
||||
.mf-btn { padding: 8px 16px; border-radius: 6px; font-size: 0.8rem; cursor: pointer; border: none; background: rgba(255,255,255,0.1); color: #e4e4e7; transition: all 0.2s; }
|
||||
.mf-btn:hover { background: rgba(255,255,255,0.15); }
|
||||
.mf-btn-danger { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||
.mf-btn-danger:hover { background: rgba(239, 68, 68, 0.3); }
|
||||
.mf-table { width: 100%; border-collapse: collapse; }
|
||||
.mf-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); background: rgba(255,255,255,0.02); }
|
||||
.mf-table td { padding: 12px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
|
||||
.mf-table tr:hover td { background: rgba(255, 255, 255, 0.03); }
|
||||
.mf-empty { text-align: center; padding: 48px 20px; color: #71717a; }
|
||||
.mf-quality { padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 600; color: white; }
|
||||
`;
|
||||
|
||||
// Time period filter
|
||||
var filterSection = E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'style': 'display: flex; gap: 10px; align-items: center; margin-bottom: 15px;' }, [
|
||||
E('label', {}, _('Time Period: ')),
|
||||
E('select', { 'id': 'time-filter', 'class': 'cbi-input-select' }, [
|
||||
E('option', { 'value': '1' }, _('Last 1 hour')),
|
||||
E('option', { 'value': '6' }, _('Last 6 hours')),
|
||||
E('option', { 'value': '24', 'selected': 'selected' }, _('Last 24 hours')),
|
||||
E('option', { 'value': '168' }, _('Last 7 days'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': function() {
|
||||
var hours = document.getElementById('time-filter').value;
|
||||
API.getStreamHistory(parseInt(hours)).then(function(data) {
|
||||
updateHistoryTable(data.history || []);
|
||||
});
|
||||
}
|
||||
}, _('Refresh')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative',
|
||||
'style': 'margin-left: auto;',
|
||||
'click': function() {
|
||||
if (confirm(_('Clear all history data?'))) {
|
||||
API.clearHistory().then(function() {
|
||||
ui.addNotification(null, E('p', _('History cleared')), 'info');
|
||||
updateHistoryTable([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, _('Clear History'))
|
||||
var container = E('div', { 'class': 'mf-page' }, [
|
||||
E('style', {}, css),
|
||||
renderMediaFlowNav('history'),
|
||||
E('div', { 'class': 'mf-header' }, [
|
||||
E('div', { 'class': 'mf-title' }, ['📜 ', _('Stream History')]),
|
||||
E('div', { 'class': 'mf-subtitle' }, _('Historical record of detected streaming sessions'))
|
||||
])
|
||||
]);
|
||||
v.appendChild(filterSection);
|
||||
|
||||
// History table
|
||||
var tableContainer = E('div', { 'id': 'history-table-container', 'class': 'cbi-section' });
|
||||
v.appendChild(tableContainer);
|
||||
// Controls
|
||||
var controls = E('div', { 'class': 'mf-controls' }, [
|
||||
E('label', {}, _('Time Period: ')),
|
||||
E('select', { 'id': 'time-filter', 'class': 'mf-select' }, [
|
||||
E('option', { 'value': '1' }, _('Last 1 hour')),
|
||||
E('option', { 'value': '6' }, _('Last 6 hours')),
|
||||
E('option', { 'value': '24', 'selected': 'selected' }, _('Last 24 hours')),
|
||||
E('option', { 'value': '168' }, _('Last 7 days'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'mf-btn',
|
||||
'click': function() {
|
||||
var hours = document.getElementById('time-filter').value;
|
||||
API.getStreamHistory(parseInt(hours)).then(function(data) {
|
||||
updateHistoryTable(data.history || []);
|
||||
});
|
||||
}
|
||||
}, _('Refresh')),
|
||||
E('button', {
|
||||
'class': 'mf-btn mf-btn-danger',
|
||||
'style': 'margin-left: auto;',
|
||||
'click': function() {
|
||||
if (confirm(_('Clear all history data?'))) {
|
||||
API.clearHistory().then(function() {
|
||||
ui.addNotification(null, E('p', _('History cleared')), 'info');
|
||||
updateHistoryTable([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, _('Clear History'))
|
||||
]);
|
||||
container.appendChild(controls);
|
||||
|
||||
var tableContainer = E('div', { 'id': 'history-table-container', 'class': 'mf-card' });
|
||||
container.appendChild(tableContainer);
|
||||
|
||||
var updateHistoryTable = function(history) {
|
||||
var container = document.getElementById('history-table-container');
|
||||
if (!container) return;
|
||||
var el = document.getElementById('history-table-container');
|
||||
if (!el) return;
|
||||
|
||||
var table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Time')),
|
||||
E('th', { 'class': 'th' }, _('Service')),
|
||||
E('th', { 'class': 'th' }, _('Category')),
|
||||
E('th', { 'class': 'th' }, _('Client')),
|
||||
E('th', { 'class': 'th' }, _('Quality')),
|
||||
E('th', { 'class': 'th' }, _('Duration')),
|
||||
E('th', { 'class': 'th' }, _('Bandwidth'))
|
||||
])
|
||||
]);
|
||||
if (!history || history.length === 0) {
|
||||
el.innerHTML = '';
|
||||
el.appendChild(E('div', { 'class': 'mf-empty' }, _('No historical data available.')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (history && history.length > 0) {
|
||||
// Sort by timestamp descending
|
||||
history.sort(function(a, b) {
|
||||
return new Date(b.timestamp) - new Date(a.timestamp);
|
||||
});
|
||||
history.sort(function(a, b) {
|
||||
return new Date(b.timestamp) - new Date(a.timestamp);
|
||||
});
|
||||
|
||||
var categoryIcons = {
|
||||
'video': '🎬',
|
||||
'audio': '🎵',
|
||||
'visio': '📹',
|
||||
'other': '📊'
|
||||
};
|
||||
var qualityColors = { 'SD': '#6b7280', 'HD': '#059669', 'FHD': '#2563eb', '4K': '#9333ea' };
|
||||
var categoryIcons = { 'video': '🎬', 'audio': '🎵', 'visio': '📹', 'other': '📊' };
|
||||
|
||||
var qualityColors = {
|
||||
'SD': '#999',
|
||||
'HD': '#0088cc',
|
||||
'FHD': '#00cc00',
|
||||
'4K': '#cc0000'
|
||||
};
|
||||
|
||||
history.slice(0, 100).forEach(function(entry) {
|
||||
var table = E('table', { 'class': 'mf-table' }, [
|
||||
E('thead', {}, [
|
||||
E('tr', {}, [
|
||||
E('th', {}, _('Time')),
|
||||
E('th', {}, _('Service')),
|
||||
E('th', {}, _('Category')),
|
||||
E('th', {}, _('Client')),
|
||||
E('th', {}, _('Quality')),
|
||||
E('th', {}, _('Duration')),
|
||||
E('th', {}, _('Bandwidth'))
|
||||
])
|
||||
]),
|
||||
E('tbody', {}, history.slice(0, 100).map(function(entry) {
|
||||
var time = new Date(entry.timestamp).toLocaleString();
|
||||
var duration = Math.floor((entry.duration || 0) / 60);
|
||||
var categoryIcon = categoryIcons[entry.category] || '📊';
|
||||
var qualityColor = qualityColors[entry.quality] || '#666';
|
||||
var qualityColor = qualityColors[entry.quality] || '#6b7280';
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, time),
|
||||
E('td', { 'class': 'td' }, entry.app || 'unknown'),
|
||||
E('td', { 'class': 'td' }, categoryIcon + ' ' + (entry.category || 'other')),
|
||||
E('td', { 'class': 'td' }, entry.client || 'unknown'),
|
||||
E('td', { 'class': 'td' },
|
||||
E('span', { 'style': 'color: ' + qualityColor + '; font-weight: bold' }, entry.quality || 'N/A')
|
||||
),
|
||||
E('td', { 'class': 'td' }, duration + ' min'),
|
||||
E('td', { 'class': 'td' }, (entry.bandwidth || 0) + ' kbps')
|
||||
]));
|
||||
});
|
||||
} else {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td', 'colspan': '7', 'style': 'text-align: center; font-style: italic; padding: 20px;' },
|
||||
_('No historical data available. Streaming sessions will appear here once detected.'))
|
||||
]));
|
||||
}
|
||||
return E('tr', {}, [
|
||||
E('td', {}, time),
|
||||
E('td', {}, E('strong', {}, entry.app || 'unknown')),
|
||||
E('td', {}, categoryIcon + ' ' + (entry.category || 'other')),
|
||||
E('td', {}, entry.client || 'unknown'),
|
||||
E('td', {}, entry.quality ? E('span', { 'class': 'mf-quality', 'style': 'background:' + qualityColor }, entry.quality) : '-'),
|
||||
E('td', {}, duration + ' min'),
|
||||
E('td', {}, (entry.bandwidth || 0) + ' kbps')
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(table);
|
||||
el.innerHTML = '';
|
||||
el.appendChild(table);
|
||||
};
|
||||
|
||||
// Initial render
|
||||
updateHistoryTable(history);
|
||||
|
||||
return v;
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
wrapper.appendChild(container);
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
@ -2,6 +2,32 @@
|
||||
'require view';
|
||||
'require ui';
|
||||
'require media-flow/api as API';
|
||||
'require secubox-portal/header as SbHeader';
|
||||
|
||||
var MEDIAFLOW_NAV = [
|
||||
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||
{ id: 'clients', icon: '👥', label: 'Clients' },
|
||||
{ id: 'services', icon: '🎬', label: 'Services' },
|
||||
{ id: 'history', icon: '📜', label: 'History' },
|
||||
{ id: 'alerts', icon: '🔔', label: 'Alerts' }
|
||||
];
|
||||
|
||||
function renderMediaFlowNav(activeId) {
|
||||
return E('div', {
|
||||
'class': 'sb-app-nav',
|
||||
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;flex-wrap:wrap;'
|
||||
}, MEDIAFLOW_NAV.map(function(item) {
|
||||
var isActive = activeId === item.id;
|
||||
return E('a', {
|
||||
'href': L.url('admin', 'secubox', 'mediaflow', item.id),
|
||||
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||
(isActive ? 'background:linear-gradient(135deg,#ec4899,#8b5cf6);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||
}, [
|
||||
E('span', {}, item.icon),
|
||||
E('span', {}, _(item.label))
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
@ -14,102 +40,107 @@ return L.view.extend({
|
||||
var statsByService = data[0] || {};
|
||||
var services = statsByService.services || {};
|
||||
|
||||
var v = E('div', { 'class': 'cbi-map' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
E('h2', {}, _('Services Statistics')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Detailed statistics per streaming service'))
|
||||
var css = `
|
||||
.mf-page { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #e4e4e7; }
|
||||
.mf-header { margin-bottom: 24px; }
|
||||
.mf-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; }
|
||||
.mf-subtitle { color: #a1a1aa; font-size: 0.875rem; }
|
||||
.mf-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; overflow: hidden; }
|
||||
.mf-table { width: 100%; border-collapse: collapse; }
|
||||
.mf-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); background: rgba(255,255,255,0.02); }
|
||||
.mf-table td { padding: 12px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
|
||||
.mf-table tr:hover td { background: rgba(255, 255, 255, 0.03); }
|
||||
.mf-empty { text-align: center; padding: 48px 20px; color: #71717a; }
|
||||
.mf-empty-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.5; }
|
||||
.mf-btn { padding: 8px 16px; border-radius: 6px; font-size: 0.8rem; cursor: pointer; border: none; background: rgba(255,255,255,0.1); color: #e4e4e7; transition: all 0.2s; }
|
||||
.mf-btn:hover { background: rgba(255,255,255,0.15); }
|
||||
`;
|
||||
|
||||
var container = E('div', { 'class': 'mf-page' }, [
|
||||
E('style', {}, css),
|
||||
renderMediaFlowNav('services'),
|
||||
E('div', { 'class': 'mf-header' }, [
|
||||
E('div', { 'class': 'mf-title' }, ['🎬 ', _('Services Statistics')]),
|
||||
E('div', { 'class': 'mf-subtitle' }, _('Detailed statistics per streaming service'))
|
||||
])
|
||||
]);
|
||||
|
||||
var servicesList = Object.keys(services);
|
||||
|
||||
if (servicesList.length === 0) {
|
||||
v.appendChild(E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', { 'style': 'font-style: italic; text-align: center; padding: 20px' },
|
||||
_('No service data available yet. Streaming services will appear here once detected.'))
|
||||
]));
|
||||
return v;
|
||||
}
|
||||
|
||||
// Sort by total duration
|
||||
servicesList.sort(function(a, b) {
|
||||
return (services[b].total_duration_seconds || 0) - (services[a].total_duration_seconds || 0);
|
||||
});
|
||||
|
||||
var table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Service')),
|
||||
E('th', { 'class': 'th' }, _('Category')),
|
||||
E('th', { 'class': 'th' }, _('Sessions')),
|
||||
E('th', { 'class': 'th' }, _('Total Duration')),
|
||||
E('th', { 'class': 'th' }, _('Avg Bandwidth')),
|
||||
E('th', { 'class': 'th' }, _('Actions'))
|
||||
])
|
||||
]);
|
||||
|
||||
servicesList.forEach(function(service) {
|
||||
var stats = services[service];
|
||||
var duration = stats.total_duration_seconds || 0;
|
||||
var hours = Math.floor(duration / 3600);
|
||||
var minutes = Math.floor((duration % 3600) / 60);
|
||||
var avgBandwidth = stats.total_bandwidth_kbps / stats.sessions || 0;
|
||||
|
||||
var categoryIcon = {
|
||||
'video': '🎬',
|
||||
'audio': '🎵',
|
||||
'visio': '📹'
|
||||
}[stats.category] || '📊';
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, categoryIcon + ' ' + service),
|
||||
E('td', { 'class': 'td' }, stats.category),
|
||||
E('td', { 'class': 'td' }, String(stats.sessions)),
|
||||
E('td', { 'class': 'td' }, hours + 'h ' + minutes + 'm'),
|
||||
E('td', { 'class': 'td' }, Math.round(avgBandwidth) + ' kbps'),
|
||||
E('td', { 'class': 'td' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': function(ev) {
|
||||
API.getServiceDetails(service).then(function(details) {
|
||||
ui.showModal(_('Service Details: ') + service, [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Category: ')),
|
||||
E('span', {}, details.category || 'unknown')
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Total Sessions: ')),
|
||||
E('span', {}, String(details.total_sessions || 0))
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Average Bandwidth: ')),
|
||||
E('span', {}, Math.round(details.avg_bandwidth_kbps || 0) + ' kbps')
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Typical Quality: ')),
|
||||
E('span', {}, details.typical_quality || 'unknown')
|
||||
]),
|
||||
E('p', {}, [
|
||||
E('strong', {}, _('Total Duration: ')),
|
||||
E('span', {}, Math.floor((details.total_duration_seconds || 0) / 3600) + 'h')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.hideModal
|
||||
}, _('Close'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
}
|
||||
}, _('Details'))
|
||||
container.appendChild(E('div', { 'class': 'mf-card' }, [
|
||||
E('div', { 'class': 'mf-empty' }, [
|
||||
E('div', { 'class': 'mf-empty-icon' }, '🎬'),
|
||||
E('div', {}, _('No service data available yet. Streaming services will appear here once detected.'))
|
||||
])
|
||||
]));
|
||||
});
|
||||
} else {
|
||||
servicesList.sort(function(a, b) {
|
||||
return (services[b].total_duration_seconds || 0) - (services[a].total_duration_seconds || 0);
|
||||
});
|
||||
|
||||
v.appendChild(E('div', { 'class': 'cbi-section' }, table));
|
||||
var table = E('table', { 'class': 'mf-table' }, [
|
||||
E('thead', {}, [
|
||||
E('tr', {}, [
|
||||
E('th', {}, _('Service')),
|
||||
E('th', {}, _('Category')),
|
||||
E('th', {}, _('Sessions')),
|
||||
E('th', {}, _('Total Duration')),
|
||||
E('th', {}, _('Avg Bandwidth')),
|
||||
E('th', {}, _('Actions'))
|
||||
])
|
||||
]),
|
||||
E('tbody', {}, servicesList.map(function(service) {
|
||||
var stats = services[service];
|
||||
var duration = stats.total_duration_seconds || 0;
|
||||
var hours = Math.floor(duration / 3600);
|
||||
var minutes = Math.floor((duration % 3600) / 60);
|
||||
var avgBandwidth = stats.total_bandwidth_kbps / stats.sessions || 0;
|
||||
|
||||
return v;
|
||||
var categoryIcon = {
|
||||
'video': '🎬',
|
||||
'audio': '🎵',
|
||||
'visio': '📹'
|
||||
}[stats.category] || '📊';
|
||||
|
||||
return E('tr', {}, [
|
||||
E('td', {}, E('strong', {}, categoryIcon + ' ' + service)),
|
||||
E('td', {}, stats.category || 'other'),
|
||||
E('td', {}, String(stats.sessions)),
|
||||
E('td', {}, hours + 'h ' + minutes + 'm'),
|
||||
E('td', {}, Math.round(avgBandwidth) + ' kbps'),
|
||||
E('td', {}, [
|
||||
E('button', {
|
||||
'class': 'mf-btn',
|
||||
'click': function() {
|
||||
API.getServiceDetails(service).then(function(details) {
|
||||
ui.showModal(_('Service Details: ') + service, [
|
||||
E('div', { 'style': 'padding: 20px; color: #e4e4e7;' }, [
|
||||
E('p', {}, [E('strong', {}, _('Category: ')), details.category || 'unknown']),
|
||||
E('p', {}, [E('strong', {}, _('Total Sessions: ')), String(details.total_sessions || 0)]),
|
||||
E('p', {}, [E('strong', {}, _('Average Bandwidth: ')), Math.round(details.avg_bandwidth_kbps || 0) + ' kbps']),
|
||||
E('p', {}, [E('strong', {}, _('Typical Quality: ')), details.typical_quality || 'unknown']),
|
||||
E('p', {}, [E('strong', {}, _('Total Duration: ')), Math.floor((details.total_duration_seconds || 0) / 3600) + 'h'])
|
||||
]),
|
||||
E('div', { 'style': 'text-align: right; padding: 10px;' }, [
|
||||
E('button', { 'class': 'mf-btn', 'click': ui.hideModal }, _('Close'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
}
|
||||
}, _('Details'))
|
||||
])
|
||||
]);
|
||||
}))
|
||||
]);
|
||||
|
||||
container.appendChild(E('div', { 'class': 'mf-card' }, table));
|
||||
}
|
||||
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
wrapper.appendChild(container);
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user