diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widget-renderer.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widget-renderer.js new file mode 100644 index 00000000..7b96851f --- /dev/null +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widget-renderer.js @@ -0,0 +1,419 @@ +'use strict'; +'require baseclass'; +'require secubox-admin.api as API'; +'require poll'; + +/** + * SecuBox Widget Renderer + * + * Provides a flexible widget system for displaying app-specific metrics + * and controls in a responsive grid layout with auto-refresh. + */ + +var WidgetRenderer = baseclass.extend({ + /** + * Initialize widget renderer + * @param {Object} options - Configuration options + * @param {string} options.containerId - DOM element ID for widget container + * @param {Array} options.apps - Apps with widget configurations + * @param {number} options.defaultRefreshInterval - Default refresh interval in seconds (default: 30) + * @param {string} options.gridMode - Grid layout mode: 'auto', 'fixed-2', 'fixed-3', 'fixed-4' (default: 'auto') + */ + __init__: function(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 = {}; + + // Register built-in templates + this.registerBuiltInTemplates(); + }, + + /** + * Register built-in widget templates + */ + registerBuiltInTemplates: function() { + var self = this; + + // Default template - simple metric display + this.registerTemplate('default', { + render: function(container, app, data) { + container.innerHTML = ''; + container.appendChild(E('div', { 'class': 'widget-default' }, [ + E('div', { 'class': 'widget-icon' }, app.icon || '📊'), + E('div', { 'class': 'widget-title' }, app.name), + E('div', { 'class': 'widget-status' }, + data.widget_enabled ? 'Widget Enabled' : 'No widget data' + ) + ])); + } + }); + + // Security widget template + this.registerTemplate('security', { + render: function(container, app, data) { + container.innerHTML = ''; + + var metrics = data.metrics || []; + var statusClass = data.status === 'ok' ? 'status-success' : + data.status === 'warning' ? 'status-warning' : + data.status === 'error' ? 'status-error' : 'status-unknown'; + + container.appendChild(E('div', { 'class': 'widget-security' }, [ + E('div', { 'class': 'widget-header' }, [ + E('div', { 'class': 'widget-icon' }, app.icon || '🔒'), + E('div', { 'class': 'widget-title' }, app.name), + E('div', { 'class': 'widget-status-indicator ' + statusClass }) + ]), + E('div', { 'class': 'widget-metrics' }, + metrics.map(function(metric) { + return self.renderMetric(metric); + }) + ), + 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 + ])); + } + }); + + // Network widget template + this.registerTemplate('network', { + render: function(container, app, data) { + container.innerHTML = ''; + + var metrics = data.metrics || []; + var connections = data.active_connections || 0; + var bandwidth = data.bandwidth || { up: 0, down: 0 }; + + container.appendChild(E('div', { 'class': 'widget-network' }, [ + E('div', { 'class': 'widget-header' }, [ + E('div', { 'class': 'widget-icon' }, app.icon || '🌐'), + E('div', { 'class': 'widget-title' }, app.name) + ]), + 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) + ' / ' + self.formatBandwidth(bandwidth.down)) + ]) + ].concat(metrics.map(function(metric) { + return self.renderMetric(metric); + }))) + ])); + } + }); + + // Monitoring widget template + this.registerTemplate('monitoring', { + render: function(container, app, data) { + container.innerHTML = ''; + + var metrics = data.metrics || []; + var statusClass = data.status === 'healthy' ? 'status-success' : + data.status === 'degraded' ? 'status-warning' : + data.status === 'down' ? 'status-error' : 'status-unknown'; + + container.appendChild(E('div', { 'class': 'widget-monitoring' }, [ + E('div', { 'class': 'widget-header' }, [ + E('div', { 'class': 'widget-icon' }, app.icon || '📈'), + E('div', { 'class': 'widget-title' }, app.name), + E('div', { 'class': 'widget-status-badge ' + statusClass }, + data.status || 'unknown') + ]), + E('div', { 'class': 'widget-metrics-grid' }, + metrics.map(function(metric) { + return self.renderMetricCard(metric); + }) + ), + data.uptime ? E('div', { 'class': 'widget-uptime' }, [ + E('span', { 'class': 'uptime-label' }, 'Uptime: '), + E('span', { 'class': 'uptime-value' }, self.formatUptime(data.uptime)) + ]) : null + ])); + } + }); + + // Hosting widget template + this.registerTemplate('hosting', { + render: function(container, app, data) { + container.innerHTML = ''; + + var metrics = data.metrics || []; + var services = data.services || []; + + container.appendChild(E('div', { 'class': 'widget-hosting' }, [ + E('div', { 'class': 'widget-header' }, [ + E('div', { 'class': 'widget-icon' }, app.icon || '🖥️'), + E('div', { 'class': 'widget-title' }, app.name) + ]), + E('div', { 'class': 'widget-services' }, + services.map(function(service) { + var statusClass = service.running ? 'service-running' : 'service-stopped'; + return E('div', { 'class': 'service-item ' + statusClass }, [ + E('span', { 'class': 'service-name' }, service.name), + E('span', { 'class': 'service-status' }, + service.running ? '✓' : '✗') + ]); + }) + ), + metrics.length > 0 ? E('div', { 'class': 'widget-metrics' }, + metrics.map(function(metric) { + return self.renderMetric(metric); + }) + ) : null + ])); + } + }); + + // Compact metric template + this.registerTemplate('compact', { + render: function(container, app, data) { + container.innerHTML = ''; + + var primaryMetric = 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), + E('div', { 'class': 'widget-value-large' }, + primaryMetric.value || '0'), + E('div', { 'class': 'widget-label-small' }, + primaryMetric.label || '') + ]) + ])); + } + }); + }, + + /** + * Register a custom widget template + * @param {string} name - Template name + * @param {Object} template - Template object with render function + */ + registerTemplate: function(name, template) { + this.templates[name] = template; + }, + + /** + * Render a single metric + * @param {Object} metric - Metric data + * @returns {Element} Metric DOM element + */ + 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 + ':'), + E('span', { 'class': valueClass }, + metric.formatted_value || metric.value?.toString() || '0') + ]); + }, + + /** + * Render a metric as a card (for grid layouts) + * @param {Object} metric - Metric data + * @returns {Element} Metric card DOM element + */ + 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), + E('div', { 'class': valueClass }, + metric.formatted_value || metric.value?.toString() || '0'), + metric.unit ? E('div', { 'class': 'metric-card-unit' }, metric.unit) : null + ]); + }, + + /** + * Initialize and render all widgets + */ + render: function() { + var container = document.getElementById(this.containerId); + if (!container) { + console.error('Widget container not found:', this.containerId); + return; + } + + // Clear container + container.innerHTML = ''; + container.className = 'widget-grid widget-grid-' + this.gridMode; + + // Filter apps that have widgets enabled + var widgetApps = this.apps.filter(function(app) { + return 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; + } + + // Render each widget + var self = this; + widgetApps.forEach(function(app) { + self.renderWidget(container, app); + }); + }, + + /** + * Render a single widget + * @param {Element} container - Parent container element + * @param {Object} app - App configuration with widget settings + */ + renderWidget: function(container, app) { + var self = this; + var widgetConfig = app.widget || {}; + var template = widgetConfig.template || 'default'; + var refreshInterval = widgetConfig.refresh_interval || this.defaultRefreshInterval; + + // Create widget container + var widgetElement = E('div', { + 'class': 'widget-item widget-' + app.category, + '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); + + // Store widget reference + this.widgets.push({ + app: app, + element: widgetElement, + contentElement: document.getElementById('widget-content-' + app.id) + }); + + // Initial data load + this.updateWidget(app, template); + + // Setup auto-refresh if interval > 0 + if (refreshInterval > 0) { + var pollHandle = poll.add(function() { + return self.updateWidget(app, template); + }, refreshInterval); + + this.pollHandles.push(pollHandle); + } + }, + + /** + * Update widget data + * @param {Object} app - App configuration + * @param {string} templateName - Template to use for rendering + */ + 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) + ])); + } + }).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 widget renderer and cleanup polling + */ + destroy: function() { + // Remove all poll handles + this.pollHandles.forEach(function(handle) { + poll.remove(handle); + }); + this.pollHandles = []; + this.widgets = []; + }, + + /** + * Format bandwidth value + * @param {number} bytesPerSec - Bytes per second + * @returns {string} Formatted string + */ + 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'; + }, + + /** + * Format uptime in seconds + * @param {number} seconds - Uptime in seconds + * @returns {string} Formatted string + */ + formatUptime: function(seconds) { + var days = Math.floor(seconds / 86400); + var hours = Math.floor((seconds % 86400) / 3600); + var mins = Math.floor((seconds % 3600) / 60); + + if (days > 0) return days + 'd ' + hours + 'h ' + mins + 'm'; + if (hours > 0) return hours + 'h ' + mins + 'm'; + return mins + 'm'; + }, + + /** + * Format timestamp + * @param {string|number} timestamp - ISO timestamp or Unix timestamp + * @returns {string} Formatted relative time + */ + formatTimestamp: function(timestamp) { + var date = typeof timestamp === 'number' ? + new Date(timestamp * 1000) : new Date(timestamp); + var now = new Date(); + var diffMs = now - date; + 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'; + } +}); + +return WidgetRenderer; diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widgets.css b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widgets.css new file mode 100644 index 00000000..ba06cb4a --- /dev/null +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widgets.css @@ -0,0 +1,509 @@ +/** + * SecuBox Widget System Styles + * Responsive grid layout for app widgets + */ + +/* Widget Grid Container */ +.widget-grid { + display: grid; + gap: 1.5rem; + margin-top: 1.5rem; +} + +/* Grid Modes */ +.widget-grid-auto { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); +} + +.widget-grid-fixed-2 { + grid-template-columns: repeat(2, 1fr); +} + +.widget-grid-fixed-3 { + grid-template-columns: repeat(3, 1fr); +} + +.widget-grid-fixed-4 { + grid-template-columns: repeat(4, 1fr); +} + +/* Responsive adjustments */ +@media (max-width: 1400px) { + .widget-grid-fixed-4 { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 1024px) { + .widget-grid-fixed-3, + .widget-grid-fixed-4 { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .widget-grid { + grid-template-columns: 1fr !important; + gap: 1rem; + } +} + +/* Widget Item */ +.widget-item { + background: var(--card-bg, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + padding: 1rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.2s, transform 0.2s; +} + +.widget-item:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +/* Widget Loading State */ +.widget-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 120px; + color: #999; +} + +.widget-loading .spinner { + width: 40px; + height: 40px; + border: 3px solid #f3f3f3; + border-top: 3px solid #0066cc; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 0.5rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Widget Error State */ +.widget-error { + text-align: center; + padding: 1.5rem; + color: #d32f2f; +} + +.widget-error .error-icon { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.widget-error .error-message { + font-weight: 600; + margin-bottom: 0.25rem; +} + +.widget-error .error-details { + font-size: 0.85rem; + color: #666; +} + +/* Widget Header */ +.widget-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border-color, #e0e0e0); +} + +.widget-icon { + font-size: 2rem; + line-height: 1; +} + +.widget-icon-small { + font-size: 1.5rem; + line-height: 1; +} + +.widget-title { + font-size: 1.1rem; + font-weight: 600; + flex: 1; + color: var(--text-primary, #333); +} + +.widget-title-small { + font-size: 0.9rem; + font-weight: 600; + color: var(--text-secondary, #666); +} + +.widget-status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + margin-left: auto; +} + +.widget-status-badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + margin-left: auto; +} + +/* Status Colors */ +.status-success, +.widget-status-badge.status-success { + background-color: #4caf50; + color: white; +} + +.status-warning, +.widget-status-badge.status-warning { + background-color: #ff9800; + color: white; +} + +.status-error, +.widget-status-badge.status-error { + background-color: #f44336; + color: white; +} + +.status-unknown, +.widget-status-badge.status-unknown { + background-color: #9e9e9e; + color: white; +} + +/* Widget Metrics */ +.widget-metrics { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.metric-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border-light, #f0f0f0); +} + +.metric-row:last-child { + border-bottom: none; +} + +.metric-label { + font-size: 0.9rem; + color: var(--text-secondary, #666); +} + +.metric-value { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary, #333); +} + +.metric-value.value-warning { + color: #ff9800; +} + +.metric-value.value-error { + color: #f44336; +} + +/* Widget Metrics Grid */ +.widget-metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: 1rem; + margin-top: 0.5rem; +} + +.metric-card { + text-align: center; + padding: 1rem; + background: var(--card-accent-bg, #f8f9fa); + border-radius: 6px; + border: 1px solid var(--border-light, #f0f0f0); +} + +.metric-card-label { + font-size: 0.8rem; + color: var(--text-secondary, #666); + margin-bottom: 0.5rem; + text-transform: uppercase; + font-weight: 500; +} + +.metric-card-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary, #333); + line-height: 1.2; +} + +.metric-card-value.value-warning { + color: #ff9800; +} + +.metric-card-value.value-error { + color: #f44336; +} + +.metric-card-unit { + font-size: 0.75rem; + color: var(--text-secondary, #666); + margin-top: 0.25rem; +} + +/* Security Widget Specific */ +.widget-security .widget-last-event { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color, #e0e0e0); + font-size: 0.85rem; + color: var(--text-secondary, #666); +} + +.widget-security .event-label { + font-weight: 500; +} + +.widget-security .event-time { + color: var(--text-primary, #333); +} + +/* Network Widget Specific */ +.widget-network .widget-metrics { + margin-top: 0.5rem; +} + +/* Monitoring Widget Specific */ +.widget-monitoring .widget-uptime { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color, #e0e0e0); + text-align: center; + font-size: 0.9rem; +} + +.widget-monitoring .uptime-label { + color: var(--text-secondary, #666); +} + +.widget-monitoring .uptime-value { + font-weight: 600; + color: var(--text-primary, #333); +} + +/* Hosting Widget Specific */ +.widget-hosting .widget-services { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.widget-hosting .service-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background: var(--card-accent-bg, #f8f9fa); + border-radius: 4px; + border-left: 3px solid transparent; +} + +.widget-hosting .service-item.service-running { + border-left-color: #4caf50; +} + +.widget-hosting .service-item.service-stopped { + border-left-color: #f44336; +} + +.widget-hosting .service-name { + font-weight: 500; + color: var(--text-primary, #333); +} + +.widget-hosting .service-status { + font-size: 1.2rem; +} + +.widget-hosting .service-running .service-status { + color: #4caf50; +} + +.widget-hosting .service-stopped .service-status { + color: #f44336; +} + +/* Compact Widget */ +.widget-compact { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem; +} + +.widget-compact .widget-content { + flex: 1; +} + +.widget-compact .widget-value-large { + font-size: 2rem; + font-weight: 700; + color: var(--accent-color, #0066cc); + line-height: 1; +} + +.widget-compact .widget-label-small { + font-size: 0.8rem; + color: var(--text-secondary, #666); + margin-top: 0.25rem; +} + +/* Default Widget */ +.widget-default { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 120px; + text-align: center; +} + +.widget-default .widget-icon { + font-size: 3rem; + margin-bottom: 0.5rem; +} + +.widget-default .widget-title { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.widget-default .widget-status { + font-size: 0.9rem; + color: var(--text-secondary, #666); +} + +/* No Widgets State */ +.no-widgets { + grid-column: 1 / -1; + text-align: center; + padding: 4rem 2rem; + color: var(--text-secondary, #666); +} + +.no-widgets-icon { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +.no-widgets h3 { + font-size: 1.5rem; + margin-bottom: 0.5rem; + color: var(--text-primary, #333); +} + +.no-widgets p { + font-size: 1rem; + max-width: 500px; + margin: 0 auto; +} + +/* Widget Category Colors */ +.widget-security { + border-left: 4px solid #f44336; +} + +.widget-network { + border-left: 4px solid #2196f3; +} + +.widget-monitoring { + border-left: 4px solid #4caf50; +} + +.widget-hosting { + border-left: 4px solid #ff9800; +} + +.widget-productivity { + border-left: 4px solid #9c27b0; +} + +/* Dark Mode Support */ +@media (prefers-color-scheme: dark) { + .widget-item { + background: #2a2a2a; + border-color: #444; + } + + .widget-header { + border-bottom-color: #444; + } + + .widget-title { + color: #e0e0e0; + } + + .metric-row { + border-bottom-color: #333; + } + + .metric-label { + color: #999; + } + + .metric-value { + color: #e0e0e0; + } + + .metric-card { + background: #333; + border-color: #444; + } + + .metric-card-value { + color: #e0e0e0; + } + + .widget-hosting .service-item { + background: #333; + } + + .no-widgets h3 { + color: #e0e0e0; + } +} + +/* Print Styles */ +@media print { + .widget-grid { + display: block; + } + + .widget-item { + break-inside: avoid; + page-break-inside: avoid; + margin-bottom: 1rem; + box-shadow: none; + border: 1px solid #ccc; + } +} diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js index c21b0559..db958186 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js @@ -2,10 +2,13 @@ 'require view'; 'require secubox-admin.api as API'; 'require secubox-admin.components as Components'; +'require secubox-admin.widget-renderer as WidgetRenderer'; 'require poll'; 'require ui'; return view.extend({ + widgetRenderer: null, + load: function() { return Promise.all([ API.getApps(), @@ -35,6 +38,8 @@ return view.extend({ 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/admin.css') }), + E('link', { 'rel': 'stylesheet', + 'href': L.resource('secubox-admin/widgets.css') }), E('h2', {}, 'Admin Control Panel'), @@ -52,6 +57,9 @@ return view.extend({ // Recent alerts this.renderAlertsSection(alerts), + // App Widgets Section + this.renderWidgetsSection(apps), + // Quick actions this.renderQuickActions() ]); @@ -59,6 +67,12 @@ return view.extend({ // Auto-refresh every 30 seconds poll.add(L.bind(this.pollData, this), 30); + // Initialize widget renderer after DOM is ready + var self = this; + requestAnimationFrame(function() { + self.initializeWidgets(apps); + }); + return container; }, @@ -163,6 +177,42 @@ return view.extend({ ]); }, + renderWidgetsSection: function(apps) { + // Filter apps with widgets enabled + var widgetApps = apps.filter(function(app) { + return app.widget && app.widget.enabled; + }); + + var widgetCount = widgetApps.length; + + return E('div', { 'class': 'widgets-section card' }, [ + E('div', { 'class': 'widgets-header' }, [ + E('h3', {}, 'App Widgets'), + E('span', { 'class': 'widget-count' }, + widgetCount + (widgetCount === 1 ? ' widget' : ' widgets')) + ]), + E('div', { 'id': 'dashboard-widgets-container' }) + ]); + }, + + initializeWidgets: function(apps) { + // Cleanup existing widget renderer + if (this.widgetRenderer) { + this.widgetRenderer.destroy(); + } + + // Create new widget renderer + this.widgetRenderer = new WidgetRenderer({ + containerId: 'dashboard-widgets-container', + apps: apps, + defaultRefreshInterval: 30, + gridMode: 'auto' + }); + + // Render widgets + this.widgetRenderer.render(); + }, + pollData: function() { var self = this; return Promise.all([ @@ -177,5 +227,13 @@ return view.extend({ handleSaveApply: null, handleSave: null, - handleReset: null + handleReset: null, + + addFooter: function() { + // Cleanup widget renderer when leaving page + if (this.widgetRenderer) { + this.widgetRenderer.destroy(); + this.widgetRenderer = null; + } + } });