'use strict'; 'require baseclass'; 'require secubox-admin/api as API'; 'require secubox-admin/chart-utils as ChartUtils'; 'require secubox-admin/realtime-client as RealtimeClient'; 'require poll'; function WidgetRendererInstance(options) { options = options || {}; this.containerId = options.containerId || 'widget-container'; this.apps = options.apps || []; this.defaultRefreshInterval = options.defaultRefreshInterval || 30; this.gridMode = options.gridMode || 'auto'; this.widgets = []; this.pollHandles = []; this.templates = {}; this.realtimeSubscriptions = []; // Initialize real-time client this.realtime = Object.create(RealtimeClient); this.realtime.init({ enableWebSocket: true, enablePolling: true, pollInterval: this.defaultRefreshInterval * 1000, debug: true }); this.registerBuiltInTemplates(); } WidgetRendererInstance.prototype = { registerBuiltInTemplates: function() { var self = this; this.registerTemplate('default', { render: function(container, app, data) { container.innerHTML = ''; var status = data && data.status ? data.status : 'unknown'; var statusClass = status === 'running' ? 'status-success' : status === 'stopped' ? 'status-warning' : status === 'not_installed' ? 'status-error' : 'status-unknown'; var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; container.appendChild(E('div', { 'class': 'widget-default' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '📊'), E('div', { 'class': 'widget-title-wrapper' }, [ E('div', { 'class': 'widget-title' }, app.name || 'Application'), version ? E('div', { 'class': 'widget-version' }, version) : null ]), E('div', { 'class': 'widget-status-indicator ' + statusClass }) ]), E('div', { 'class': 'widget-status-text' }, [ E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-status' }, data && data.widget_enabled ? 'Widget Enabled' : 'No widget data' ) ])); } }); this.registerTemplate('security', { render: function(container, app, data) { container.innerHTML = ''; var metrics = data && data.metrics ? data.metrics : []; var status = data && data.status ? data.status : 'unknown'; var statusClass = status === 'running' ? 'status-success' : status === 'stopped' ? 'status-warning' : status === 'not_installed' ? 'status-error' : 'status-unknown'; var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; container.appendChild(E('div', { 'class': 'widget-security' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '🔒'), E('div', { 'class': 'widget-title-wrapper' }, [ E('div', { 'class': 'widget-title' }, app.name || 'Security'), version ? E('div', { 'class': 'widget-version' }, version) : null ]), E('div', { 'class': 'widget-status-indicator ' + statusClass }) ]), E('div', { 'class': 'widget-status-text' }, [ E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-metrics' }, metrics.map(function(metric) { return self.renderMetric(metric || {}); }) ), data && data.last_event ? E('div', { 'class': 'widget-last-event' }, [ E('span', { 'class': 'event-label' }, 'Last Event: '), E('span', { 'class': 'event-time' }, self.formatTimestamp(data.last_event)) ]) : null ])); } }); this.registerTemplate('network', { render: function(container, app, data) { container.innerHTML = ''; var metrics = data && data.metrics ? data.metrics : []; var connections = data && data.active_connections ? data.active_connections : 0; var bandwidth = data && data.bandwidth ? data.bandwidth : { up: 0, down: 0 }; var status = data && data.status ? data.status : 'unknown'; var statusClass = status === 'running' ? 'status-success' : status === 'stopped' ? 'status-warning' : status === 'not_installed' ? 'status-error' : 'status-unknown'; var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; container.appendChild(E('div', { 'class': 'widget-network' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '🌐'), E('div', { 'class': 'widget-title-wrapper' }, [ E('div', { 'class': 'widget-title' }, app.name || 'Network'), version ? E('div', { 'class': 'widget-version' }, version) : null ]), E('div', { 'class': 'widget-status-indicator ' + statusClass }) ]), E('div', { 'class': 'widget-status-text' }, [ E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-metrics' }, [ E('div', { 'class': 'metric-row' }, [ E('span', { 'class': 'metric-label' }, 'Connections:'), E('span', { 'class': 'metric-value' }, connections.toString()) ]), E('div', { 'class': 'metric-row' }, [ E('span', { 'class': 'metric-label' }, 'Up/Down:'), E('span', { 'class': 'metric-value' }, self.formatBandwidth(bandwidth.up || 0) + ' / ' + self.formatBandwidth(bandwidth.down || 0)) ]) ].concat(metrics.map(function(metric) { return self.renderMetric(metric || {}); }))) ])); } }); this.registerTemplate('monitoring', { render: function(container, app, data) { container.innerHTML = ''; var metrics = data && data.metrics ? data.metrics : []; var status = data && data.status ? data.status : 'unknown'; var statusClass = status === 'running' ? 'status-success' : status === 'stopped' ? 'status-warning' : status === 'not_installed' ? 'status-error' : 'status-unknown'; var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; container.appendChild(E('div', { 'class': 'widget-monitoring' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '📈'), E('div', { 'class': 'widget-title-wrapper' }, [ E('div', { 'class': 'widget-title' }, app.name || 'Monitoring'), version ? E('div', { 'class': 'widget-version' }, version) : null ]), E('div', { 'class': 'widget-status-indicator ' + statusClass }) ]), E('div', { 'class': 'widget-status-text' }, [ E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-metrics-grid' }, metrics.map(function(metric) { return self.renderMetricCard(metric || {}); }) ), data && data.uptime ? E('div', { 'class': 'widget-uptime' }, [ E('span', { 'class': 'uptime-label' }, 'Uptime: '), E('span', { 'class': 'uptime-value' }, self.formatUptime(data.uptime)) ]) : null ])); } }); this.registerTemplate('hosting', { render: function(container, app, data) { container.innerHTML = ''; var metrics = data && data.metrics ? data.metrics : []; var services = data && data.services ? data.services : []; var status = data && data.status ? data.status : 'unknown'; var statusClass = status === 'running' ? 'status-success' : status === 'stopped' ? 'status-warning' : status === 'not_installed' ? 'status-error' : 'status-unknown'; var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; container.appendChild(E('div', { 'class': 'widget-hosting' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '🖥️'), E('div', { 'class': 'widget-title-wrapper' }, [ E('div', { 'class': 'widget-title' }, app.name || 'Hosting'), version ? E('div', { 'class': 'widget-version' }, version) : null ]), E('div', { 'class': 'widget-status-indicator ' + statusClass }) ]), E('div', { 'class': 'widget-status-text' }, [ E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-services' }, services.map(function(service) { var running = service && service.running; var statusClass = running ? 'service-running' : 'service-stopped'; return E('div', { 'class': 'service-item ' + statusClass }, [ E('span', { 'class': 'service-name' }, (service && service.name) || 'Service'), E('span', { 'class': 'service-status' }, running ? '✓' : '✗') ]); }) ), metrics.length > 0 ? E('div', { 'class': 'widget-metrics' }, metrics.map(function(metric) { return self.renderMetric(metric || {}); }) ) : null ])); } }); this.registerTemplate('compact', { render: function(container, app, data) { container.innerHTML = ''; var primaryMetric = data && data.primary_metric ? data.primary_metric : {}; container.appendChild(E('div', { 'class': 'widget-compact' }, [ E('div', { 'class': 'widget-icon-small' }, app.icon || '📊'), E('div', { 'class': 'widget-content' }, [ E('div', { 'class': 'widget-title-small' }, app.name || 'Metric'), E('div', { 'class': 'widget-value-large' }, primaryMetric.value || '0'), E('div', { 'class': 'widget-label-small' }, primaryMetric.label || '') ]) ])); } }); this.registerTemplate('chart-timeseries', { render: function(container, app, data) { container.innerHTML = ''; var chartContainer = E('div', { 'class': 'cyber-chart-container cyber-chart-container--md' }, [ E('div', { 'class': 'cyber-chart-header' }, [ E('div', {}, [ E('div', { 'class': 'cyber-chart-title' }, app.name || 'Timeseries'), E('div', { 'class': 'cyber-chart-subtitle' }, (data && data.subtitle) || 'Time Series Data') ]) ]), E('div', { 'class': 'cyber-chart-canvas' }, [ E('canvas', { 'id': 'chart-' + app.id }) ]) ]); container.appendChild(chartContainer); var canvas = chartContainer.querySelector('canvas'); var chartData = { labels: data && data.labels ? data.labels : [], datasets: (data && data.datasets ? data.datasets : []).map(function(ds) { return { label: ds && ds.label ? ds.label : 'Dataset', data: ds && ds.data ? ds.data : [] }; }) }; ChartUtils.createLineChart(canvas, chartData, {}).catch(function(err) { console.error('[WIDGET] Chart error:', err); }); } }); this.registerTemplate('chart-gauge', { render: function(container, app, data) { container.innerHTML = ''; var value = data && data.value ? data.value : 0; var max = data && data.max ? data.max : 100; var percentage = max > 0 ? Math.round((value / max) * 100) : 0; var chartContainer = E('div', { 'class': 'cyber-chart-container cyber-chart-container--sm' }, [ E('div', { 'class': 'cyber-chart-header' }, [ E('div', { 'class': 'cyber-chart-title' }, app.name || 'Gauge') ]), E('div', { 'class': 'cyber-chart-metrics' }, [ E('div', { 'class': 'cyber-chart-metric' }, [ E('div', { 'class': 'cyber-chart-metric-label' }, (data && data.label) || 'Usage'), E('div', { 'class': 'cyber-chart-metric-value' }, percentage + '%') ]) ]), E('div', { 'class': 'cyber-chart-canvas' }, [ E('canvas', { 'id': 'chart-' + app.id }) ]) ]); container.appendChild(chartContainer); var canvas = chartContainer.querySelector('canvas'); ChartUtils.createGaugeChart(canvas, percentage, {}).catch(function(err) { console.error('[WIDGET] Gauge error:', err); }); } }); this.registerTemplate('sparkline', { render: function(container, app, data) { container.innerHTML = ''; var values = data && data.values ? data.values : []; var currentValue = values.length ? values[values.length - 1] : 0; var previousValue = values.length > 1 ? values[values.length - 2] : 0; var change = currentValue - previousValue; var trendClass = change > 0 ? 'cyber-sparkline-trend--up' : 'cyber-sparkline-trend--down'; var sparklineContainer = E('div', { 'class': 'cyber-sparkline' }, [ E('div', { 'class': 'cyber-sparkline-canvas' }, [ E('canvas', { 'id': 'sparkline-' + app.id }) ]), E('div', { 'class': 'cyber-sparkline-value' }, currentValue.toString()), E('div', { 'class': 'cyber-sparkline-trend ' + trendClass }, change > 0 ? '↑ ' + change : '↓ ' + Math.abs(change)) ]); container.appendChild(sparklineContainer); var canvas = sparklineContainer.querySelector('canvas'); ChartUtils.createSparkline(canvas, values).catch(function(err) { console.error('[WIDGET] Sparkline error:', err); }); } }); }, registerTemplate: function(name, template) { this.templates[name] = template; }, renderMetric: function(metric) { var valueClass = 'metric-value'; if (metric.status === 'warning') valueClass += ' value-warning'; if (metric.status === 'error') valueClass += ' value-error'; return E('div', { 'class': 'metric-row' }, [ E('span', { 'class': 'metric-label' }, (metric.label || 'Metric') + ':'), E('span', { 'class': valueClass }, getMetricValue(metric)) ]); }, renderMetricCard: function(metric) { var valueClass = 'metric-card-value'; if (metric.status === 'warning') valueClass += ' value-warning'; if (metric.status === 'error') valueClass += ' value-error'; return E('div', { 'class': 'metric-card' }, [ E('div', { 'class': 'metric-card-label' }, metric.label || 'Metric'), E('div', { 'class': valueClass }, getMetricValue(metric)), metric.unit ? E('div', { 'class': 'metric-card-unit' }, metric.unit) : null ]); }, render: function() { var container = document.getElementById(this.containerId); if (!container) { console.error('Widget container not found:', this.containerId); return; } container.innerHTML = ''; container.className = 'widget-grid widget-grid-' + this.gridMode; var widgetApps = this.apps.filter(function(app) { return app && app.widget && app.widget.enabled; }); if (widgetApps.length === 0) { container.appendChild(E('div', { 'class': 'no-widgets' }, [ E('div', { 'class': 'no-widgets-icon' }, '📊'), E('h3', {}, 'No Active Widgets'), E('p', {}, 'Install and enable apps with widget support to see live metrics here.') ])); return; } var self = this; widgetApps.forEach(function(app) { self.renderWidget(container, app); }); }, renderWidget: function(container, app) { var self = this; var widgetConfig = app.widget || {}; var templateName = widgetConfig.template || 'default'; var refreshInterval = widgetConfig.refresh_interval || this.defaultRefreshInterval; var widgetElement = E('div', { 'class': 'widget-item widget-' + (app.category || 'general'), 'data-app-id': app.id }, [ E('div', { 'class': 'widget-content', 'id': 'widget-content-' + app.id }, [ E('div', { 'class': 'widget-loading' }, [ E('div', { 'class': 'spinner' }), E('p', {}, 'Loading...') ]) ]) ]); container.appendChild(widgetElement); this.widgets.push({ app: app, element: widgetElement, contentElement: document.getElementById('widget-content-' + app.id) }); this.updateWidget(app, templateName); if (refreshInterval > 0) { var channel = 'widget.' + app.id; // Subscribe to real-time updates var unsubscribe = this.realtime.subscribe(channel, function(data) { console.log('[WIDGET] Real-time update for', app.id, data); var contentElement = document.getElementById('widget-content-' + app.id); if (!contentElement) return; var templateObj = self.templates[templateName] || self.templates['default']; try { templateObj.render(contentElement, app, data || {}); } catch (error) { console.error('Widget render error for', app.id, error); contentElement.innerHTML = ''; contentElement.appendChild(E('div', { 'class': 'widget-error' }, [ E('div', { 'class': 'error-icon' }, '⚠️'), E('div', { 'class': 'error-message' }, 'Widget Error'), E('div', { 'class': 'error-details' }, error.message || 'Render failure') ])); } }); this.realtimeSubscriptions.push({ channel: channel, unsubscribe: unsubscribe }); // Keep poll as fallback (realtime-client will use it if WebSocket unavailable) var pollHandle = poll.add(function() { return self.updateWidget(app, templateName); }, refreshInterval); this.pollHandles.push(pollHandle); } }, updateWidget: function(app, templateName) { var self = this; return API.getWidgetData(app.id).then(function(data) { var contentElement = document.getElementById('widget-content-' + app.id); if (!contentElement) { return; } var template = self.templates[templateName] || self.templates.default; try { template.render(contentElement, app, data || {}); } catch (error) { console.error('Widget render error for', app.id, error); contentElement.innerHTML = ''; contentElement.appendChild(E('div', { 'class': 'widget-error' }, [ E('div', { 'class': 'error-icon' }, '⚠️'), E('div', { 'class': 'error-message' }, 'Widget Error'), E('div', { 'class': 'error-details' }, error.message || 'Render failure') ])); } }).catch(function(error) { console.error('Failed to load widget data for', app.id, error); var contentElement = document.getElementById('widget-content-' + app.id); if (contentElement) { contentElement.innerHTML = ''; contentElement.appendChild(E('div', { 'class': 'widget-error' }, [ E('div', { 'class': 'error-icon' }, '⚠️'), E('div', { 'class': 'error-message' }, 'Data Load Failed') ])); } }); }, destroy: function() { var container = document.getElementById(this.containerId); if (container) { container.innerHTML = ''; } // Cleanup real-time subscriptions this.realtimeSubscriptions.forEach(function(sub) { if (sub && sub.unsubscribe) { sub.unsubscribe(); } }); this.realtimeSubscriptions = []; // Disconnect real-time client if (this.realtime) { this.realtime.disconnect(); } // Cleanup polling fallback this.pollHandles.forEach(function(handle) { poll.remove(handle); }); this.pollHandles = []; this.widgets = []; }, formatBandwidth: function(bytesPerSec) { if (bytesPerSec < 1024) return bytesPerSec + ' B/s'; if (bytesPerSec < 1024 * 1024) return (bytesPerSec / 1024).toFixed(1) + ' KB/s'; if (bytesPerSec < 1024 * 1024 * 1024) return (bytesPerSec / (1024 * 1024)).toFixed(1) + ' MB/s'; return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + ' GB/s'; }, formatUptime: function(seconds) { var totalSeconds = parseInt(seconds, 10) || 0; var days = Math.floor(totalSeconds / 86400); var hours = Math.floor((totalSeconds % 86400) / 3600); var mins = Math.floor((totalSeconds % 3600) / 60); if (days > 0) return days + 'd ' + hours + 'h ' + mins + 'm'; if (hours > 0) return hours + 'h ' + mins + 'm'; return mins + 'm'; }, formatTimestamp: function(timestamp) { if (!timestamp) { return 'Unknown'; } var date = typeof timestamp === 'number' ? new Date(timestamp * 1000) : new Date(timestamp); var now = new Date(); var diffMs = now.getTime() - date.getTime(); var diffSecs = Math.floor(diffMs / 1000); if (diffSecs < 60) return 'Just now'; if (diffSecs < 3600) return Math.floor(diffSecs / 60) + ' min ago'; if (diffSecs < 86400) return Math.floor(diffSecs / 3600) + ' hr ago'; return Math.floor(diffSecs / 86400) + ' days ago'; } }; function getMetricValue(metric) { if (metric.formatted_value) { return metric.formatted_value; } if (metric.value === 0 || metric.value) { return metric.value.toString(); } return '0'; } return baseclass.extend({ create: function(options) { return new WidgetRendererInstance(options); } });