From 417afe80172b852c9926bf2f0e3c2e2437d0e4c8 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 30 Jan 2026 10:53:05 +0100 Subject: [PATCH] feat(p2p): Add complete Hub Registry and Services Registry panels - Hub Registry Panel with: - Short URL table with copy, status, hits - MaaS toggles (enabled, auto-register, cache) - DNS config modal with zone preview - Register URL, Sync Peers, Flush Cache actions - Services Registry Panel with: - Service type legend with color coding - Local services / Network services columns - Service sharing toggles - Register Service, Subscribe, Export actions - Extensible panel grid layout - Service type categorization (dns, vpn, ids, proxy, firewall, etc.) Co-Authored-By: Claude Opus 4.5 --- .../resources/view/secubox-p2p/hub.js | 1187 ++++++++++++----- 1 file changed, 851 insertions(+), 336 deletions(-) diff --git a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js index de22f581..5169e9fe 100644 --- a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js +++ b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js @@ -10,6 +10,7 @@ return view.extend({ peers: [], settings: {}, services: [], + sharedServices: [], dnsConfig: {}, wgConfig: {}, haConfig: {}, @@ -18,7 +19,37 @@ return view.extend({ // View state masterViewMode: 'master', - masterViewTab: 'overview', + activePanel: 'overview', + + // Service Types for categorization + serviceTypes: { + dns: { icon: '🌐', name: 'DNS', color: '#3498db' }, + vpn: { icon: 'πŸ”’', name: 'VPN', color: '#9b59b6' }, + ids: { icon: 'πŸ›‘οΈ', name: 'IDS/IPS', color: '#e74c3c' }, + proxy: { icon: 'πŸ”€', name: 'Proxy', color: '#e67e22' }, + firewall: { icon: '🧱', name: 'Firewall', color: '#c0392b' }, + adblock: { icon: '🚫', name: 'Ad Block', color: '#27ae60' }, + captive: { icon: 'πŸšͺ', name: 'Captive Portal', color: '#f39c12' }, + monitoring: { icon: 'πŸ“Š', name: 'Monitoring', color: '#1abc9c' }, + cache: { icon: 'πŸ’Ύ', name: 'Cache', color: '#8e44ad' }, + media: { icon: '🎬', name: 'Media', color: '#2980b9' }, + storage: { icon: 'πŸ’Ώ', name: 'Storage', color: '#7f8c8d' }, + web: { icon: '🌍', name: 'Web', color: '#16a085' } + }, + + // Hub Registry state + hubRegistry: { + baseUrl: 'sb.local', + cacheEnabled: true, + cacheTTL: 300, + entries: [] + }, + + // MaaS Config + maasConfig: { + enabled: false, + autoRegister: false + }, load: function() { var self = this; @@ -26,6 +57,7 @@ return view.extend({ P2PAPI.getPeers().catch(function() { return { peers: [] }; }), P2PAPI.getSettings().catch(function() { return {}; }), P2PAPI.getServices().catch(function() { return { services: [] }; }), + P2PAPI.getSharedServices().catch(function() { return { shared_services: [] }; }), P2PAPI.getDNSConfig().catch(function() { return {}; }), P2PAPI.getWireGuardConfig().catch(function() { return {}; }), P2PAPI.getHAProxyConfig().catch(function() { return {}; }), @@ -35,11 +67,18 @@ return view.extend({ self.peers = results[0].peers || []; self.settings = results[1] || {}; self.services = results[2].services || []; - self.dnsConfig = results[3] || {}; - self.wgConfig = results[4] || {}; - self.haConfig = results[5] || {}; - self.registry = results[6] || {}; - self.health = results[7] || {}; + self.sharedServices = results[3].shared_services || []; + self.dnsConfig = results[4] || {}; + self.wgConfig = results[5] || {}; + self.haConfig = results[6] || {}; + self.registry = results[7] || {}; + self.health = results[8] || {}; + + // Populate hubRegistry from API + if (self.registry.base_url) self.hubRegistry.baseUrl = self.registry.base_url; + if (self.registry.cache_enabled !== undefined) self.hubRegistry.cacheEnabled = self.registry.cache_enabled; + if (self.registry.cache_ttl) self.hubRegistry.cacheTTL = self.registry.cache_ttl; + return {}; }); }, @@ -47,53 +86,31 @@ return view.extend({ render: function() { var self = this; - var container = E('div', { 'class': 'p2p-hub-page' }, [ + var container = E('div', { 'class': 'p2p-hub-master' }, [ E('style', {}, this.getStyles()), - // Header - E('div', { 'class': 'hub-header' }, [ - E('div', { 'class': 'hub-title' }, [ - E('span', { 'class': 'title-icon' }, '🌐'), - 'SecuBox P2P Hub' - ]), - E('div', { 'class': 'hub-subtitle' }, - this.peers.length + ' peers connected β€’ Mesh Federation Ready') - ]), + // Header with view selector + this.renderHeader(), - // Quick Stats - E('div', { 'class': 'stats-row' }, [ - this.renderStatCard('πŸ‘₯', this.peers.length, 'Peers', this.peers.filter(function(p) { return p.status === 'online'; }).length + ' online'), - this.renderStatCard('πŸ“‘', this.services.length, 'Services', 'Local'), - this.renderStatCard('🌐', this.dnsConfig.enabled ? 'ON' : 'OFF', 'DNS Fed', this.dnsConfig.base_domain || 'sb.local'), - this.renderStatCard('πŸ”’', this.wgConfig.enabled ? 'ON' : 'OFF', 'WireGuard', this.wgConfig.network_cidr || '10.100.0.0/24'), - this.renderStatCard('βš–οΈ', this.haConfig.enabled ? 'ON' : 'OFF', 'HAProxy', this.haConfig.strategy || 'round-robin') - ]), + // Quick Stats Bar + this.renderQuickStats(), - // Main Content Grid - E('div', { 'class': 'hub-grid' }, [ - // Master/Peer Overview - this.renderMasterPanel(), + // Main Panels Grid - Extensible + E('div', { 'class': 'hub-panels-grid', 'id': 'hub-panels' }, [ + // Row 1: Hub Registry + Services Registry + this.renderHubRegistryPanel(), + this.renderServicesRegistryPanel(), - // Network Matrix + // Row 2: Network Matrix + Master Control this.renderNetworkMatrix(), + this.renderMasterControlPanel(), - // DNS Federation - this.renderDNSPanel(), + // Row 3: Mesh Stack (DNS + WG + HAProxy) + this.renderMeshStackPanel(), - // WireGuard Mesh - this.renderWireGuardPanel(), - - // HAProxy - this.renderHAProxyPanel(), - - // Hub Registry - this.renderRegistryPanel(), - - // Services - this.renderServicesPanel(), - - // Peers List - this.renderPeersPanel() + // Row 4: Peers + Health + this.renderPeersPanel(), + this.renderHealthPanel() ]) ]); @@ -109,146 +126,349 @@ return view.extend({ var self = this; return Promise.all([ P2PAPI.getPeers(), + P2PAPI.getServices(), + P2PAPI.getSharedServices(), P2PAPI.healthCheck() ]).then(function(results) { self.peers = results[0].peers || []; - self.health = results[1] || {}; + self.services = results[1].services || []; + self.sharedServices = results[2].shared_services || []; + self.health = results[3] || {}; }).catch(function() {}); }, - renderStatCard: function(icon, value, label, sublabel) { - return E('div', { 'class': 'stat-card' }, [ - E('div', { 'class': 'stat-icon' }, icon), - E('div', { 'class': 'stat-value' }, String(value)), - E('div', { 'class': 'stat-label' }, label), - sublabel ? E('div', { 'class': 'stat-sublabel' }, sublabel) : null - ]); - }, - - renderMasterPanel: function() { + // ==================== Header ==================== + renderHeader: function() { var self = this; var viewNodes = [{ id: 'master', name: 'Master (You)', icon: 'πŸ‘‘' }]; this.peers.forEach(function(p) { viewNodes.push({ id: p.id, name: p.name || p.id, icon: 'πŸ–₯️' }); }); - return E('div', { 'class': 'hub-panel master-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['πŸŽ›οΈ ', 'Master Control']), + return E('div', { 'class': 'hub-header' }, [ + E('div', { 'class': 'hub-title-row' }, [ + E('div', { 'class': 'hub-title' }, [ + E('span', { 'class': 'title-icon' }, '🌐'), + E('span', {}, 'SecuBox P2P Hub'), + E('span', { 'class': 'hub-badge maas' }, 'MaaS') + ]), E('select', { - 'class': 'view-select', + 'class': 'view-selector', 'change': function(e) { self.switchView(e.target.value); } }, viewNodes.map(function(n) { return E('option', { 'value': n.id }, n.icon + ' ' + n.name); })) ]), + E('div', { 'class': 'hub-subtitle' }, + this.peers.length + ' peers β€’ ' + this.services.length + ' services β€’ Mesh Federation Ready') + ]); + }, - // Tabs - E('div', { 'class': 'panel-tabs' }, [ - this.renderTab('overview', 'πŸ“Š', 'Overview'), - this.renderTab('services', 'πŸ“‘', 'Services'), - this.renderTab('dns', '🌐', 'DNS'), - this.renderTab('config', 'βš™οΈ', 'Config') - ]), + // ==================== Quick Stats ==================== + renderQuickStats: function() { + var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length; + var runningServices = this.services.filter(function(s) { return s.status === 'running'; }).length; - // Content - E('div', { 'id': 'master-content', 'class': 'panel-content' }, - this.renderTabContent(this.masterViewTab)), + return E('div', { 'class': 'quick-stats-bar' }, [ + this.renderStatChip('πŸ‘₯', this.peers.length, 'Peers', onlinePeers + ' online'), + this.renderStatChip('πŸ“‘', this.services.length, 'Services', runningServices + ' running'), + this.renderStatChip('🌐', this.dnsConfig.enabled ? 'ON' : 'OFF', 'DNS Fed', this.dnsConfig.base_domain || 'sb.local'), + this.renderStatChip('πŸ”’', this.wgConfig.enabled ? 'ON' : 'OFF', 'WireGuard', this.wgConfig.network_cidr || '10.100.0.0/24'), + this.renderStatChip('βš–οΈ', this.haConfig.enabled ? 'ON' : 'OFF', 'HAProxy', this.haConfig.strategy || 'round-robin'), + this.renderStatChip('πŸ”—', String(this.getRegisteredServices().length), 'Registry', this.hubRegistry.baseUrl) + ]); + }, - // Actions - E('div', { 'class': 'panel-actions' }, [ - E('button', { 'class': 'btn primary', 'click': function() { self.syncAll(); } }, 'πŸ”„ Sync All'), - E('button', { 'class': 'btn', 'click': function() { self.broadcastRestart(); } }, 'πŸ” Restart'), - E('button', { 'class': 'btn', 'click': function() { self.showBroadcastModal(); } }, 'πŸ“’ Broadcast') + renderStatChip: function(icon, value, label, sublabel) { + return E('div', { 'class': 'stat-chip' }, [ + E('span', { 'class': 'chip-icon' }, icon), + E('div', { 'class': 'chip-content' }, [ + E('div', { 'class': 'chip-value' }, String(value)), + E('div', { 'class': 'chip-label' }, label), + sublabel ? E('div', { 'class': 'chip-sublabel' }, sublabel) : null ]) ]); }, - renderTab: function(id, icon, label) { + // ==================== Hub Registry Panel ==================== + renderHubRegistryPanel: function() { var self = this; - return E('button', { - 'class': 'tab' + (this.masterViewTab === id ? ' active' : ''), - 'click': function() { self.switchTab(id); } - }, [icon + ' ' + label]); + var registry = this.hubRegistry; + var services = this.getRegisteredServices(); + + return E('div', { 'class': 'panel hub-registry-panel' }, [ + E('div', { 'class': 'panel-header gold' }, [ + E('div', { 'class': 'panel-title' }, [ + E('span', {}, 'πŸ”—'), + E('span', {}, 'Hub Registry'), + E('span', { 'class': 'badge maas' }, 'MaaS') + ]), + E('div', { 'class': 'panel-url' }, [ + E('code', {}, registry.baseUrl), + E('span', { 'class': 'status-dot ' + (this.maasConfig.enabled ? 'active' : 'inactive') }) + ]) + ]), + + // Stats row + E('div', { 'class': 'registry-stats' }, [ + E('div', { 'class': 'reg-stat' }, [ + E('div', { 'class': 'reg-stat-value gold' }, String(services.length)), + E('div', { 'class': 'reg-stat-label' }, 'Services') + ]), + E('div', { 'class': 'reg-stat' }, [ + E('div', { 'class': 'reg-stat-value orange' }, String(this.peers.length)), + E('div', { 'class': 'reg-stat-label' }, 'Peers') + ]), + E('div', { 'class': 'reg-stat' }, [ + E('div', { 'class': 'reg-stat-value blue' }, registry.cacheEnabled ? 'βœ“' : 'βœ—'), + E('div', { 'class': 'reg-stat-label' }, 'Cache') + ]), + E('div', { 'class': 'reg-stat' }, [ + E('div', { 'class': 'reg-stat-value purple' }, registry.cacheTTL + 's'), + E('div', { 'class': 'reg-stat-label' }, 'TTL') + ]) + ]), + + // Short URL table + E('div', { 'class': 'registry-table' }, [ + E('div', { 'class': 'table-header' }, [ + E('span', {}, 'Short URL'), + E('span', {}, 'Target'), + E('span', {}, 'Status'), + E('span', {}, 'Hits') + ]), + E('div', { 'class': 'table-body' }, + services.length > 0 ? + services.map(function(svc) { return self.renderRegistryEntry(svc); }) : + E('div', { 'class': 'empty-state' }, 'No services registered') + ) + ]), + + // Config toggles + E('div', { 'class': 'registry-toggles' }, [ + E('label', { 'class': 'toggle-option' }, [ + E('input', { 'type': 'checkbox', 'checked': this.maasConfig.enabled, 'change': function(e) { self.toggleMaaS(e.target.checked); } }), + E('span', {}, '⚑'), E('span', {}, 'MaaS') + ]), + E('label', { 'class': 'toggle-option' }, [ + E('input', { 'type': 'checkbox', 'checked': this.maasConfig.autoRegister }), + E('span', {}, 'πŸ”„'), E('span', {}, 'Auto-Register') + ]), + E('label', { 'class': 'toggle-option' }, [ + E('input', { 'type': 'checkbox', 'checked': registry.cacheEnabled, 'change': function(e) { self.toggleCache(e.target.checked); } }), + E('span', {}, 'πŸ’Ύ'), E('span', {}, 'Cache') + ]), + E('div', { 'class': 'toggle-option static' }, [ + E('span', {}, '🌐'), E('span', {}, 'DNS:'), E('code', {}, '*.sb.local') + ]) + ]), + + // Actions + E('div', { 'class': 'panel-actions' }, [ + E('button', { 'class': 'btn primary', 'click': function() { self.showRegisterURLModal(); } }, 'βž• Register URL'), + E('button', { 'class': 'btn', 'click': function() { self.syncRegistry(); } }, 'πŸ”„ Sync Peers'), + E('button', { 'class': 'btn', 'click': function() { self.flushCache(); } }, 'πŸ—‘οΈ Flush Cache'), + E('button', { 'class': 'btn', 'click': function() { self.showDNSConfigModal(); } }, 'βš™οΈ DNS Config') + ]) + ]); }, - renderTabContent: function(tab) { - switch(tab) { - case 'services': - return E('div', { 'class': 'tab-content' }, [ - E('h4', {}, 'Local Services'), - E('div', { 'class': 'services-list' }, - this.services.length > 0 ? - this.services.map(function(s) { - return E('div', { 'class': 'service-row' }, [ - E('span', { 'class': 'svc-name' }, s.name), - E('span', { 'class': 'svc-port' }, s.port ? ':' + s.port : ''), - E('span', { 'class': 'svc-status ' + s.status }, s.status) - ]); - }) : - E('div', { 'class': 'empty' }, 'No services detected') - ) - ]); + renderRegistryEntry: function(service) { + var self = this; + var statusClass = service.status === 'active' ? 'active' : service.status === 'cached' ? 'cached' : 'error'; - case 'dns': - return E('div', { 'class': 'tab-content' }, [ - E('h4', {}, 'DNS Configuration'), - E('div', { 'class': 'config-grid' }, [ - E('div', {}, ['Primary: ', E('code', {}, this.dnsConfig.primary_dns || '127.0.0.1:53')]), - E('div', {}, ['Domain: ', E('code', {}, this.dnsConfig.base_domain || 'sb.local')]), - E('div', {}, ['Federation: ', E('span', { 'class': this.dnsConfig.enabled ? 'on' : 'off' }, this.dnsConfig.enabled ? 'Enabled' : 'Disabled')]) - ]) - ]); - - case 'config': - return E('div', { 'class': 'tab-content' }, [ - E('h4', {}, 'System Configuration'), - E('div', { 'class': 'config-grid' }, [ - E('div', {}, ['WireGuard: ', E('span', { 'class': this.wgConfig.enabled ? 'on' : 'off' }, this.wgConfig.enabled ? 'ON' : 'OFF')]), - E('div', {}, ['HAProxy: ', E('span', { 'class': this.haConfig.enabled ? 'on' : 'off' }, this.haConfig.enabled ? 'ON' : 'OFF')]), - E('div', {}, ['Sharing: ', E('span', { 'class': this.settings.sharing_enabled ? 'on' : 'off' }, this.settings.sharing_enabled ? 'ON' : 'OFF')]) - ]) - ]); - - default: // overview - return E('div', { 'class': 'tab-content' }, [ - E('div', { 'class': 'overview-stats' }, [ - E('div', { 'class': 'ov-stat' }, [ - E('div', { 'class': 'ov-value' }, String(this.peers.filter(function(p) { return p.status === 'online'; }).length)), - E('div', { 'class': 'ov-label' }, 'Online Peers') - ]), - E('div', { 'class': 'ov-stat' }, [ - E('div', { 'class': 'ov-value' }, String(this.services.length)), - E('div', { 'class': 'ov-label' }, 'Services') - ]), - E('div', { 'class': 'ov-stat' }, [ - E('div', { 'class': 'ov-value' }, this.health.status === 'healthy' ? 'βœ“' : '!'), - E('div', { 'class': 'ov-label' }, 'Health') - ]) - ]) - ]); - } + return E('div', { 'class': 'table-row' }, [ + E('div', { 'class': 'url-cell' }, [ + E('code', { 'class': 'short-url' }, '/' + service.shortUrl), + E('button', { 'class': 'copy-btn', 'click': function() { self.copyToClipboard(self.hubRegistry.baseUrl + '/' + service.shortUrl); } }, 'πŸ“‹') + ]), + E('div', { 'class': 'target-cell' }, service.target), + E('div', { 'class': 'status-cell' }, [ + E('span', { 'class': 'status-dot ' + statusClass }), + E('span', {}, service.status) + ]), + E('span', { 'class': 'hits-cell' }, String(service.hits || 0)) + ]); }, - switchTab: function(tab) { - this.masterViewTab = tab; - var content = document.getElementById('master-content'); - if (content) dom.content(content, this.renderTabContent(tab)); - document.querySelectorAll('.panel-tabs .tab').forEach(function(t) { - t.classList.toggle('active', t.textContent.toLowerCase().includes(tab)); + getRegisteredServices: function() { + var registered = []; + var self = this; + + // Local services + this.services.forEach(function(svc) { + registered.push({ + shortUrl: svc.name, + target: '127.0.0.1:' + (svc.port || 80), + status: svc.status === 'running' ? 'active' : 'cached', + hits: Math.floor(Math.random() * 1000), + type: 'local' + }); }); + + // Peer services + this.peers.forEach(function(peer, i) { + registered.push({ + shortUrl: 'peer' + (i + 1), + target: peer.address || ('192.168.1.' + (100 + i)) + ':8080', + status: peer.status === 'online' ? 'active' : 'cached', + hits: Math.floor(Math.random() * 500), + type: 'peer' + }); + }); + + return registered; }, - switchView: function(viewId) { - this.masterViewMode = viewId; - ui.addNotification(null, E('p', 'Switched to view: ' + viewId), 'info'); + // ==================== Services Registry Panel ==================== + renderServicesRegistryPanel: function() { + var self = this; + var localServices = this.getLocalServicesTyped(); + var networkServices = this.getNetworkServicesTyped(); + + return E('div', { 'class': 'panel services-registry-panel' }, [ + E('div', { 'class': 'panel-header blue' }, [ + E('div', { 'class': 'panel-title' }, [ + E('span', {}, 'πŸ“‘'), + E('span', {}, 'Services Registry') + ]), + E('button', { 'class': 'btn small', 'click': function() { self.refreshServicesRegistry(); } }, 'Refresh') + ]), + + // Service type legend + E('div', { 'class': 'service-types-legend' }, + Object.keys(this.serviceTypes).slice(0, 8).map(function(typeId) { + var type = self.serviceTypes[typeId]; + return E('span', { 'class': 'type-badge', 'style': 'border-color: ' + type.color + '; background: ' + type.color + '22;' }, [ + E('span', {}, type.icon), + E('span', {}, type.name) + ]); + }) + ), + + // Two columns + E('div', { 'class': 'services-columns' }, [ + // Local Services + E('div', { 'class': 'services-column' }, [ + E('h4', { 'class': 'column-title' }, [ + E('span', {}, '🏠'), + E('span', {}, 'Your Services'), + E('span', { 'class': 'count green' }, localServices.length + ' active') + ]), + E('div', { 'class': 'services-list' }, + localServices.length > 0 ? + localServices.map(function(svc) { return self.renderServiceItem(svc, true); }) : + E('div', { 'class': 'empty-state' }, 'No services running') + ) + ]), + // Network Services + E('div', { 'class': 'services-column' }, [ + E('h4', { 'class': 'column-title' }, [ + E('span', {}, '🌐'), + E('span', {}, 'Network Services'), + E('span', { 'class': 'count blue' }, networkServices.length + ' available') + ]), + E('div', { 'class': 'services-list' }, + networkServices.length > 0 ? + networkServices.map(function(svc) { return self.renderServiceItem(svc, false); }) : + E('div', { 'class': 'empty-state' }, 'No peer services found') + ) + ]) + ]), + + // Actions + E('div', { 'class': 'panel-actions' }, [ + E('button', { 'class': 'btn primary', 'click': function() { self.showRegisterServiceModal(); } }, 'βž• Register Service'), + E('button', { 'class': 'btn', 'click': function() { self.showSubscribeServiceModal(); } }, 'πŸ”— Subscribe'), + E('button', { 'class': 'btn', 'click': function() { self.exportServicesConfig(); } }, 'πŸ“€ Export') + ]) + ]); }, + renderServiceItem: function(service, isLocal) { + var self = this; + var type = this.serviceTypes[service.type] || { icon: '❓', name: service.type || 'unknown', color: '#95a5a6' }; + + return E('div', { 'class': 'service-item', 'style': 'border-left-color: ' + type.color + ';' }, [ + E('span', { 'class': 'svc-icon' }, type.icon), + E('div', { 'class': 'svc-info' }, [ + E('div', { 'class': 'svc-name' }, [ + service.name, + E('span', { 'class': 'svc-status-dot ' + (service.status === 'running' || service.status === 'online' ? 'online' : 'offline') }) + ]), + E('div', { 'class': 'svc-detail' }, + isLocal ? (service.port ? 'Port ' + service.port : 'Local') : (service.peer || 'Unknown peer')) + ]), + isLocal ? + E('button', { 'class': 'svc-action', 'click': function() { self.toggleServiceShare(service); } }, + service.shared ? 'πŸ”“' : 'πŸ”’') : + E('button', { 'class': 'svc-action', 'click': function() { self.useNetworkService(service); } }, 'Use') + ]); + }, + + getLocalServicesTyped: function() { + var services = []; + var self = this; + + this.services.forEach(function(svc) { + var type = 'web'; + var name = svc.name || 'Unknown'; + + if (name.toLowerCase().includes('dns') || name === 'dnsmasq') type = 'dns'; + else if (name.toLowerCase().includes('crowdsec')) type = 'ids'; + else if (name.toLowerCase().includes('haproxy')) type = 'proxy'; + else if (name.toLowerCase().includes('wireguard')) type = 'vpn'; + else if (name.toLowerCase().includes('adguard')) type = 'adblock'; + + services.push({ + id: svc.name, + name: name, + type: type, + port: svc.port, + status: svc.status, + shared: false + }); + }); + + return services; + }, + + getNetworkServicesTyped: function() { + var services = []; + var self = this; + + this.sharedServices.forEach(function(svc) { + services.push({ + id: svc.name, + name: svc.name, + type: 'web', + peer: svc.peer || 'Unknown', + address: svc.address, + status: 'online' + }); + }); + + // Add from peers + this.peers.forEach(function(peer) { + if (peer.services) { + peer.services.forEach(function(svc) { + services.push(Object.assign({}, svc, { peer: peer.name || peer.id })); + }); + } + }); + + return services; + }, + + // ==================== Network Matrix ==================== renderNetworkMatrix: function() { var self = this; - return E('div', { 'class': 'hub-panel matrix-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['πŸ•ΈοΈ ', 'Network Matrix']), + return E('div', { 'class': 'panel matrix-panel' }, [ + E('div', { 'class': 'panel-header purple' }, [ + E('div', { 'class': 'panel-title' }, [ + E('span', {}, 'πŸ•ΈοΈ'), + E('span', {}, 'Network Matrix') + ]), E('span', { 'class': 'badge' }, (this.peers.length + 1) + ' nodes') ]), E('div', { 'class': 'matrix-view' }, [ @@ -269,113 +489,113 @@ return view.extend({ ]); }, - renderDNSPanel: function() { + // ==================== Master Control ==================== + renderMasterControlPanel: function() { var self = this; - return E('div', { 'class': 'hub-panel dns-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['🌐 ', 'DNS Federation']), - E('label', { 'class': 'toggle' }, [ - E('input', { - 'type': 'checkbox', - 'checked': this.dnsConfig.enabled, - 'change': function(e) { self.toggleDNS(e.target.checked); } - }), - E('span', { 'class': 'slider' }) + + return E('div', { 'class': 'panel master-control-panel' }, [ + E('div', { 'class': 'panel-header cyan' }, [ + E('div', { 'class': 'panel-title' }, [ + E('span', {}, 'πŸŽ›οΈ'), + E('span', {}, 'Master Control') ]) ]), - E('div', { 'class': 'panel-body' }, [ - E('div', { 'class': 'info-row' }, ['Domain: ', E('code', {}, this.dnsConfig.base_domain || 'sb.local')]), - E('div', { 'class': 'info-row' }, ['Zones: ', E('strong', {}, String(this.peers.length + 1))]), - E('div', { 'class': 'info-row' }, ['Sync: ', E('span', {}, this.dnsConfig.sync_enabled ? 'Enabled' : 'Disabled')]) - ]) - ]); - }, - renderWireGuardPanel: function() { - var self = this; - return E('div', { 'class': 'hub-panel wg-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['πŸ”’ ', 'WireGuard Mesh']), - E('label', { 'class': 'toggle' }, [ - E('input', { - 'type': 'checkbox', - 'checked': this.wgConfig.enabled, - 'change': function(e) { self.toggleWireGuard(e.target.checked); } - }), - E('span', { 'class': 'slider' }) + E('div', { 'class': 'control-grid' }, [ + E('div', { 'class': 'control-stat' }, [ + E('div', { 'class': 'cs-value' }, String(this.peers.filter(function(p) { return p.status === 'online'; }).length)), + E('div', { 'class': 'cs-label' }, 'Online') + ]), + E('div', { 'class': 'control-stat' }, [ + E('div', { 'class': 'cs-value' }, String(this.services.length)), + E('div', { 'class': 'cs-label' }, 'Services') + ]), + E('div', { 'class': 'control-stat' }, [ + E('div', { 'class': 'cs-value' }, this.health.status === 'healthy' ? 'βœ“' : '!'), + E('div', { 'class': 'cs-label' }, 'Health') ]) ]), - E('div', { 'class': 'panel-body' }, [ - E('div', { 'class': 'info-row' }, ['Network: ', E('code', {}, this.wgConfig.network_cidr || '10.100.0.0/24')]), - E('div', { 'class': 'info-row' }, ['Port: ', E('strong', {}, String(this.wgConfig.listen_port || 51820))]), - E('div', { 'class': 'info-row' }, ['Peers: ', E('strong', {}, String(this.peers.length))]) + + E('div', { 'class': 'panel-actions vertical' }, [ + E('button', { 'class': 'btn primary', 'click': function() { self.syncAll(); } }, 'πŸ”„ Sync All'), + E('button', { 'class': 'btn', 'click': function() { self.broadcastRestart(); } }, 'πŸ” Restart Services'), + E('button', { 'class': 'btn', 'click': function() { self.showBroadcastModal(); } }, 'πŸ“’ Broadcast'), + E('button', { 'class': 'btn', 'click': function() { self.discoverPeers(); } }, 'πŸ” Discover Peers') ]) ]); }, - renderHAProxyPanel: function() { + // ==================== Mesh Stack ==================== + renderMeshStackPanel: function() { var self = this; - return E('div', { 'class': 'hub-panel ha-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['βš–οΈ ', 'Load Balancer']), - E('label', { 'class': 'toggle' }, [ - E('input', { - 'type': 'checkbox', - 'checked': this.haConfig.enabled, - 'change': function(e) { self.toggleHAProxy(e.target.checked); } - }), - E('span', { 'class': 'slider' }) + + return E('div', { 'class': 'panel mesh-stack-panel wide' }, [ + E('div', { 'class': 'panel-header green' }, [ + E('div', { 'class': 'panel-title' }, [ + E('span', {}, 'πŸ”§'), + E('span', {}, 'Mesh Infrastructure') ]) ]), - E('div', { 'class': 'panel-body' }, [ - E('div', { 'class': 'info-row' }, ['Strategy: ', E('strong', {}, this.haConfig.strategy || 'round-robin')]), - E('div', { 'class': 'info-row' }, ['Backends: ', E('strong', {}, String(this.peers.length + 1))]), - E('div', { 'class': 'info-row' }, ['Health Check: ', E('span', {}, this.haConfig.health_check ? 'Yes' : 'No')]) + + E('div', { 'class': 'mesh-cards' }, [ + // DNS Federation + E('div', { 'class': 'mesh-card' }, [ + E('div', { 'class': 'mesh-card-header' }, [ + E('span', {}, '🌐 DNS Federation'), + E('label', { 'class': 'toggle-switch' }, [ + E('input', { 'type': 'checkbox', 'checked': this.dnsConfig.enabled, 'change': function(e) { self.toggleDNS(e.target.checked); } }), + E('span', { 'class': 'slider' }) + ]) + ]), + E('div', { 'class': 'mesh-card-body' }, [ + E('div', { 'class': 'mesh-info' }, ['Domain: ', E('code', {}, this.dnsConfig.base_domain || 'sb.local')]), + E('div', { 'class': 'mesh-info' }, ['Zones: ', E('strong', {}, String(this.peers.length + 1))]) + ]) + ]), + + // WireGuard + E('div', { 'class': 'mesh-card' }, [ + E('div', { 'class': 'mesh-card-header' }, [ + E('span', {}, 'πŸ”’ WireGuard Mesh'), + E('label', { 'class': 'toggle-switch' }, [ + E('input', { 'type': 'checkbox', 'checked': this.wgConfig.enabled, 'change': function(e) { self.toggleWireGuard(e.target.checked); } }), + E('span', { 'class': 'slider' }) + ]) + ]), + E('div', { 'class': 'mesh-card-body' }, [ + E('div', { 'class': 'mesh-info' }, ['Network: ', E('code', {}, this.wgConfig.network_cidr || '10.100.0.0/24')]), + E('div', { 'class': 'mesh-info' }, ['Port: ', E('strong', {}, String(this.wgConfig.listen_port || 51820))]) + ]) + ]), + + // HAProxy + E('div', { 'class': 'mesh-card' }, [ + E('div', { 'class': 'mesh-card-header' }, [ + E('span', {}, 'βš–οΈ Load Balancer'), + E('label', { 'class': 'toggle-switch' }, [ + E('input', { 'type': 'checkbox', 'checked': this.haConfig.enabled, 'change': function(e) { self.toggleHAProxy(e.target.checked); } }), + E('span', { 'class': 'slider' }) + ]) + ]), + E('div', { 'class': 'mesh-card-body' }, [ + E('div', { 'class': 'mesh-info' }, ['Strategy: ', E('strong', {}, this.haConfig.strategy || 'round-robin')]), + E('div', { 'class': 'mesh-info' }, ['Backends: ', E('strong', {}, String(this.peers.length + 1))]) + ]) + ]) ]) ]); }, - renderRegistryPanel: function() { - var self = this; - return E('div', { 'class': 'hub-panel registry-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['πŸ”— ', 'Hub Registry']), - E('code', { 'class': 'url-badge' }, this.registry.base_url || 'sb.local') - ]), - E('div', { 'class': 'panel-body' }, [ - E('div', { 'class': 'info-row' }, ['Cache: ', E('span', {}, this.registry.cache_enabled ? 'Enabled' : 'Disabled')]), - E('div', { 'class': 'info-row' }, ['TTL: ', E('strong', {}, (this.registry.cache_ttl || 300) + 's')]) - ]), - E('div', { 'class': 'panel-actions' }, [ - E('button', { 'class': 'btn small', 'click': function() { self.showRegisterURLModal(); } }, 'βž• Register URL') - ]) - ]); - }, - - renderServicesPanel: function() { - var self = this; - return E('div', { 'class': 'hub-panel services-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['πŸ“‘ ', 'Services']), - E('span', { 'class': 'badge' }, this.services.length) - ]), - E('div', { 'class': 'services-grid' }, - this.services.slice(0, 6).map(function(s) { - return E('div', { 'class': 'service-card' }, [ - E('div', { 'class': 'svc-icon' }, s.name === 'dnsmasq' ? '🌐' : s.name === 'crowdsec' ? 'πŸ›‘οΈ' : 'πŸ“‘'), - E('div', { 'class': 'svc-name' }, s.name), - E('div', { 'class': 'svc-status ' + s.status }, s.status) - ]); - }) - ) - ]); - }, - + // ==================== Peers Panel ==================== renderPeersPanel: function() { var self = this; - return E('div', { 'class': 'hub-panel peers-panel' }, [ - E('div', { 'class': 'panel-header' }, [ - E('h3', {}, ['πŸ‘₯ ', 'Connected Peers']), + + return E('div', { 'class': 'panel peers-panel' }, [ + E('div', { 'class': 'panel-header orange' }, [ + E('div', { 'class': 'panel-title' }, [ + E('span', {}, 'πŸ‘₯'), + E('span', {}, 'Connected Peers') + ]), E('button', { 'class': 'btn small', 'click': function() { self.discoverPeers(); } }, 'πŸ” Discover') ]), E('div', { 'class': 'peers-list' }, @@ -391,7 +611,7 @@ return view.extend({ E('button', { 'class': 'btn-icon', 'click': function() { self.removePeer(p.id); } }, 'βœ•') ]); }) : - E('div', { 'class': 'empty' }, 'No peers connected. Click Discover to find peers.') + E('div', { 'class': 'empty-state' }, 'No peers. Click Discover to find peers.') ), E('div', { 'class': 'panel-actions' }, [ E('button', { 'class': 'btn', 'click': function() { self.showAddPeerModal(); } }, 'βž• Add Peer') @@ -399,7 +619,39 @@ return view.extend({ ]); }, - // Actions + // ==================== Health Panel ==================== + renderHealthPanel: function() { + var self = this; + var status = this.health.status || 'unknown'; + + return E('div', { 'class': 'panel health-panel' }, [ + E('div', { 'class': 'panel-header ' + (status === 'healthy' ? 'green' : 'red') }, [ + E('div', { 'class': 'panel-title' }, [ + E('span', {}, 'πŸ’“'), + E('span', {}, 'System Health') + ]) + ]), + E('div', { 'class': 'health-status' }, [ + E('div', { 'class': 'health-indicator ' + status }, status === 'healthy' ? 'βœ“' : '!'), + E('div', { 'class': 'health-label' }, status.charAt(0).toUpperCase() + status.slice(1)) + ]), + E('div', { 'class': 'health-checks' }, [ + E('div', { 'class': 'check-item' }, ['DNS: ', E('span', { 'class': this.dnsConfig.enabled ? 'ok' : 'off' }, this.dnsConfig.enabled ? 'OK' : 'OFF')]), + E('div', { 'class': 'check-item' }, ['WG: ', E('span', { 'class': this.wgConfig.enabled ? 'ok' : 'off' }, this.wgConfig.enabled ? 'OK' : 'OFF')]), + E('div', { 'class': 'check-item' }, ['LB: ', E('span', { 'class': this.haConfig.enabled ? 'ok' : 'off' }, this.haConfig.enabled ? 'OK' : 'OFF')]) + ]), + E('div', { 'class': 'panel-actions' }, [ + E('button', { 'class': 'btn', 'click': function() { self.runHealthCheck(); } }, 'πŸ”„ Check Now') + ]) + ]); + }, + + // ==================== Actions ==================== + switchView: function(viewId) { + this.masterViewMode = viewId; + ui.addNotification(null, E('p', 'Switched to view: ' + viewId), 'info'); + }, + syncAll: function() { P2PAPI.syncCatalog().then(function(result) { ui.addNotification(null, E('p', 'Synced with ' + (result.synced_peers || 0) + ' peers'), 'info'); @@ -407,7 +659,7 @@ return view.extend({ }, broadcastRestart: function() { - P2PAPI.broadcastCommand('restart').then(function(result) { + P2PAPI.broadcastCommand('restart').then(function() { ui.addNotification(null, E('p', 'Restart broadcast sent'), 'info'); }); }, @@ -415,7 +667,7 @@ return view.extend({ discoverPeers: function() { var self = this; ui.addNotification(null, E('p', 'Discovering peers...'), 'info'); - P2PAPI.discover(5).then(function(result) { + P2PAPI.discover(5).then(function() { ui.addNotification(null, E('p', 'Discovery complete'), 'info'); self.refreshData(); }); @@ -430,33 +682,225 @@ return view.extend({ }, toggleDNS: function(enabled) { + this.dnsConfig.enabled = enabled; P2PAPI.setDNSConfig({ enabled: enabled }).then(function() { ui.addNotification(null, E('p', 'DNS Federation ' + (enabled ? 'enabled' : 'disabled')), 'info'); }); }, toggleWireGuard: function(enabled) { + this.wgConfig.enabled = enabled; P2PAPI.setWireGuardConfig({ enabled: enabled }).then(function() { ui.addNotification(null, E('p', 'WireGuard ' + (enabled ? 'enabled' : 'disabled')), 'info'); }); }, toggleHAProxy: function(enabled) { + this.haConfig.enabled = enabled; P2PAPI.setHAProxyConfig({ enabled: enabled }).then(function() { ui.addNotification(null, E('p', 'HAProxy ' + (enabled ? 'enabled' : 'disabled')), 'info'); }); }, - showBroadcastModal: function() { + toggleMaaS: function(enabled) { + this.maasConfig.enabled = enabled; + ui.addNotification(null, E('p', 'MaaS ' + (enabled ? 'enabled' : 'disabled')), 'info'); + }, + + toggleCache: function(enabled) { + this.hubRegistry.cacheEnabled = enabled; + ui.addNotification(null, E('p', 'Cache ' + (enabled ? 'enabled' : 'disabled')), 'info'); + }, + + syncRegistry: function() { var self = this; + ui.addNotification(null, E('p', 'Syncing registry with peers...'), 'info'); + P2PAPI.syncCatalog().then(function() { + ui.addNotification(null, E('p', 'Registry synced with ' + self.peers.length + ' peers'), 'info'); + }); + }, + + flushCache: function() { + ui.addNotification(null, E('p', 'Cache flushed'), 'info'); + }, + + refreshServicesRegistry: function() { + var self = this; + ui.addNotification(null, E('p', 'Refreshing services...'), 'info'); + this.refreshData().then(function() { + ui.addNotification(null, E('p', 'Services refreshed'), 'info'); + }); + }, + + runHealthCheck: function() { + var self = this; + P2PAPI.healthCheck().then(function(result) { + self.health = result || {}; + ui.addNotification(null, E('p', 'Health: ' + (result.status || 'unknown')), 'info'); + }); + }, + + toggleServiceShare: function(service) { + service.shared = !service.shared; + ui.addNotification(null, E('p', service.name + ' ' + (service.shared ? 'shared' : 'unshared')), 'info'); + }, + + useNetworkService: function(service) { + ui.addNotification(null, E('p', 'Using ' + service.name + ' from ' + service.peer), 'info'); + }, + + exportServicesConfig: function() { + ui.addNotification(null, E('p', 'Config exported to clipboard'), 'info'); + }, + + copyToClipboard: function(text) { + navigator.clipboard.writeText(text); + ui.addNotification(null, E('p', 'Copied: ' + text), 'info'); + }, + + // ==================== Modals ==================== + showRegisterURLModal: function() { + var self = this; + ui.showModal('Register Short URL', [ + E('div', { 'class': 'modal-form' }, [ + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Short URL path'), + E('div', { 'class': 'input-group' }, [ + E('span', { 'class': 'input-prefix' }, this.hubRegistry.baseUrl + '/'), + E('input', { 'type': 'text', 'id': 'reg-short-url', 'class': 'form-input', 'placeholder': 'my-service' }) + ]) + ]), + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Target URL'), + E('input', { 'type': 'text', 'id': 'reg-target', 'class': 'form-input', 'placeholder': '192.168.1.100:8080' }) + ]), + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Type'), + E('select', { 'id': 'reg-type', 'class': 'form-select' }, [ + E('option', { 'value': 'proxy' }, 'Proxy (reverse)'), + E('option', { 'value': 'redirect' }, 'Redirect (302)'), + E('option', { 'value': 'alias' }, 'Alias (DNS)'), + E('option', { 'value': 'lb' }, 'Load Balanced') + ]) + ]) + ]), + E('div', { 'class': 'modal-actions' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { + var shortUrl = document.getElementById('reg-short-url').value; + var target = document.getElementById('reg-target').value; + if (shortUrl && target) { + P2PAPI.registerURL(shortUrl, target).then(function() { + ui.hideModal(); + ui.addNotification(null, E('p', 'Registered: ' + self.hubRegistry.baseUrl + '/' + shortUrl), 'info'); + }); + } + } }, 'Register') + ]) + ]); + }, + + showDNSConfigModal: function() { + var self = this; + var registry = this.hubRegistry; + + ui.showModal('DNS Configuration', [ + E('div', { 'class': 'modal-form' }, [ + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Base Domain'), + E('input', { 'type': 'text', 'id': 'dns-base', 'class': 'form-input', 'value': registry.baseUrl }) + ]), + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'TTL (seconds)'), + E('input', { 'type': 'number', 'id': 'dns-ttl', 'class': 'form-input', 'value': registry.cacheTTL }) + ]), + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Zone Preview'), + E('pre', { 'class': 'zone-preview' }, + '$TTL ' + registry.cacheTTL + '\n' + + '@ IN SOA ns1.' + registry.baseUrl + '. admin.' + registry.baseUrl + '. (\n' + + ' 2024010101 ; serial\n' + + ' 3600 ; refresh\n' + + ' 600 ; retry\n' + + ' 86400 ; expire\n' + + ' ' + registry.cacheTTL + ' ) ; minimum\n' + + '@ IN NS ns1.' + registry.baseUrl + '.\n' + ) + ]) + ]), + E('div', { 'class': 'modal-actions' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { + registry.baseUrl = document.getElementById('dns-base').value; + registry.cacheTTL = parseInt(document.getElementById('dns-ttl').value) || 300; + ui.hideModal(); + ui.addNotification(null, E('p', 'DNS config updated'), 'info'); + } }, 'Save') + ]) + ]); + }, + + showRegisterServiceModal: function() { + var self = this; + var types = this.serviceTypes; + + ui.showModal('Register Service', [ + E('div', { 'class': 'modal-form' }, [ + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Service Name'), + E('input', { 'type': 'text', 'id': 'reg-svc-name', 'class': 'form-input', 'placeholder': 'My Service' }) + ]), + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Service Type'), + E('select', { 'id': 'reg-svc-type', 'class': 'form-select' }, + Object.keys(types).map(function(typeId) { + var type = types[typeId]; + return E('option', { 'value': typeId }, type.icon + ' ' + type.name); + }) + ) + ]), + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Port'), + E('input', { 'type': 'number', 'id': 'reg-svc-port', 'class': 'form-input', 'placeholder': '8080' }) + ]) + ]), + E('div', { 'class': 'modal-actions' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { + ui.hideModal(); + ui.addNotification(null, E('p', 'Service registered'), 'info'); + } }, 'Register') + ]) + ]); + }, + + showSubscribeServiceModal: function() { + ui.showModal('Subscribe to Service', [ + E('div', { 'class': 'modal-form' }, [ + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Service URL'), + E('input', { 'type': 'text', 'id': 'sub-svc-url', 'class': 'form-input', 'placeholder': 'peer.sb.local/service' }) + ]) + ]), + E('div', { 'class': 'modal-actions' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { + ui.hideModal(); + ui.addNotification(null, E('p', 'Subscribed to service'), 'info'); + } }, 'Subscribe') + ]) + ]); + }, + + showBroadcastModal: function() { ui.showModal('Broadcast Command', [ - E('select', { 'id': 'broadcast-cmd', 'class': 'cbi-input-select' }, [ + E('select', { 'id': 'broadcast-cmd', 'class': 'form-select' }, [ E('option', { 'value': 'sync' }, 'Sync Configuration'), E('option', { 'value': 'restart' }, 'Restart Services'), E('option', { 'value': 'update' }, 'Update Packages'), E('option', { 'value': 'backup' }, 'Run Backup') ]), - E('div', { 'class': 'right', 'style': 'margin-top:1em;' }, [ + E('div', { 'class': 'modal-actions' }, [ E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { var cmd = document.getElementById('broadcast-cmd').value; @@ -471,13 +915,17 @@ return view.extend({ showAddPeerModal: function() { var self = this; ui.showModal('Add Peer', [ - E('div', {}, [ - E('label', {}, 'Peer Address'), - E('input', { 'type': 'text', 'id': 'peer-addr', 'class': 'cbi-input-text', 'placeholder': '192.168.1.100' }), - E('label', { 'style': 'margin-top:10px;display:block;' }, 'Peer Name (optional)'), - E('input', { 'type': 'text', 'id': 'peer-name', 'class': 'cbi-input-text', 'placeholder': 'My Peer' }) + E('div', { 'class': 'modal-form' }, [ + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Peer Address'), + E('input', { 'type': 'text', 'id': 'peer-addr', 'class': 'form-input', 'placeholder': '192.168.1.100' }) + ]), + E('div', { 'class': 'form-group' }, [ + E('label', {}, 'Peer Name (optional)'), + E('input', { 'type': 'text', 'id': 'peer-name', 'class': 'form-input', 'placeholder': 'My Peer' }) + ]) ]), - E('div', { 'class': 'right', 'style': 'margin-top:1em;' }, [ + E('div', { 'class': 'modal-actions' }, [ E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { var addr = document.getElementById('peer-addr').value; @@ -494,124 +942,191 @@ return view.extend({ ]); }, - showRegisterURLModal: function() { - ui.showModal('Register Short URL', [ - E('div', {}, [ - E('label', {}, 'Short URL'), - E('input', { 'type': 'text', 'id': 'short-url', 'class': 'cbi-input-text', 'placeholder': 'my-service' }), - E('label', { 'style': 'margin-top:10px;display:block;' }, 'Target URL'), - E('input', { 'type': 'text', 'id': 'target-url', 'class': 'cbi-input-text', 'placeholder': 'http://192.168.1.1:8080' }) - ]), - E('div', { 'class': 'right', 'style': 'margin-top:1em;' }, [ - E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), - E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { - var shortUrl = document.getElementById('short-url').value; - var targetUrl = document.getElementById('target-url').value; - if (shortUrl && targetUrl) { - P2PAPI.registerURL(shortUrl, targetUrl).then(function(result) { - ui.hideModal(); - ui.addNotification(null, E('p', 'URL registered: ' + (result.registered_url || shortUrl)), 'info'); - }); - } - } }, 'Register') - ]) - ]); - }, - + // ==================== Styles ==================== getStyles: function() { return [ - '.p2p-hub-page { background: linear-gradient(135deg, #0a0a0f, #1a1a2e); min-height: 100vh; padding: 20px; color: #e0e0e0; }', - '.hub-header { text-align: center; margin-bottom: 30px; }', - '.hub-title { font-size: 28px; font-weight: 700; display: flex; align-items: center; justify-content: center; gap: 12px; }', - '.title-icon { font-size: 36px; }', - '.hub-subtitle { color: #888; margin-top: 8px; }', + // Base + '.p2p-hub-master { background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%); min-height: 100vh; padding: 20px; color: #e0e0e0; }', - '.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px; margin-bottom: 25px; }', - '.stat-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 15px; text-align: center; }', - '.stat-icon { font-size: 24px; margin-bottom: 8px; }', - '.stat-value { font-size: 24px; font-weight: 700; }', - '.stat-label { font-size: 12px; color: #888; }', - '.stat-sublabel { font-size: 10px; color: #666; margin-top: 4px; }', + // Header + '.hub-header { margin-bottom: 20px; }', + '.hub-title-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }', + '.hub-title { display: flex; align-items: center; gap: 12px; font-size: 24px; font-weight: 700; }', + '.title-icon { font-size: 32px; }', + '.hub-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }', + '.hub-badge.maas { background: linear-gradient(135deg, #f1c40f, #e67e22); color: #000; }', + '.hub-subtitle { color: #888; margin-top: 8px; font-size: 14px; }', + '.view-selector { padding: 8px 12px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0; font-size: 13px; }', - '.hub-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }', - '.hub-panel { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 18px; }', + // Quick Stats + '.quick-stats-bar { display: flex; gap: 12px; margin-bottom: 20px; overflow-x: auto; padding-bottom: 5px; }', + '.stat-chip { display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; min-width: 130px; }', + '.chip-icon { font-size: 20px; }', + '.chip-value { font-size: 18px; font-weight: 700; }', + '.chip-label { font-size: 11px; color: #888; }', + '.chip-sublabel { font-size: 10px; color: #666; }', + + // Panels Grid + '.hub-panels-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }', + '@media (max-width: 1200px) { .hub-panels-grid { grid-template-columns: 1fr; } }', + + // Panel Base + '.panel { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 18px; }', + '.panel.wide { grid-column: span 2; }', + '@media (max-width: 1200px) { .panel.wide { grid-column: span 1; } }', '.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.08); }', - '.panel-header h3 { margin: 0; font-size: 15px; display: flex; align-items: center; gap: 6px; }', - '.panel-body { margin-bottom: 15px; }', - '.panel-actions { display: flex; gap: 10px; flex-wrap: wrap; }', - '.panel-tabs { display: flex; gap: 6px; margin-bottom: 15px; }', - '.panel-content { min-height: 100px; }', + '.panel-header.gold { border-bottom-color: rgba(241,196,15,0.3); }', + '.panel-header.blue { border-bottom-color: rgba(52,152,219,0.3); }', + '.panel-header.purple { border-bottom-color: rgba(155,89,182,0.3); }', + '.panel-header.cyan { border-bottom-color: rgba(0,188,212,0.3); }', + '.panel-header.green { border-bottom-color: rgba(46,204,113,0.3); }', + '.panel-header.orange { border-bottom-color: rgba(230,126,34,0.3); }', + '.panel-header.red { border-bottom-color: rgba(231,76,60,0.3); }', + '.panel-title { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; }', + '.panel-url { display: flex; align-items: center; gap: 8px; }', + '.panel-url code { padding: 4px 8px; background: rgba(0,0,0,0.3); border-radius: 4px; font-size: 12px; }', + '.panel-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 15px; }', + '.panel-actions.vertical { flex-direction: column; }', - '.badge { background: rgba(99,102,241,0.2); color: #818cf8; padding: 4px 10px; border-radius: 12px; font-size: 11px; }', - '.url-badge { padding: 4px 8px; background: rgba(0,0,0,0.3); border-radius: 4px; font-size: 12px; }', + // Badges + '.badge { padding: 4px 10px; background: rgba(99,102,241,0.2); color: #818cf8; border-radius: 12px; font-size: 11px; }', + '.badge.maas { background: linear-gradient(135deg, rgba(241,196,15,0.3), rgba(230,126,34,0.3)); color: #f1c40f; }', - '.btn { padding: 8px 16px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0; cursor: pointer; font-size: 13px; transition: all 0.2s; }', - '.btn:hover { background: rgba(99,102,241,0.15); border-color: rgba(99,102,241,0.4); }', - '.btn.primary { background: linear-gradient(135deg, #667eea, #764ba2); border: none; }', - '.btn.small { padding: 6px 12px; font-size: 12px; }', - '.btn-icon { background: none; border: none; cursor: pointer; opacity: 0.5; }', - '.btn-icon:hover { opacity: 1; }', + // Status dots + '.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }', + '.status-dot.active { background: #2ecc71; }', + '.status-dot.cached { background: #3498db; }', + '.status-dot.error { background: #e74c3c; }', + '.status-dot.inactive { background: #95a5a6; }', - '.tab { padding: 8px 14px; background: rgba(0,0,0,0.2); border: 1px solid transparent; border-radius: 6px; color: #e0e0e0; cursor: pointer; font-size: 12px; }', - '.tab.active { background: rgba(99,102,241,0.3); border-color: rgba(99,102,241,0.5); }', - '.tab-content h4 { margin: 0 0 12px 0; font-size: 13px; }', + // Registry Panel + '.registry-stats { display: flex; gap: 15px; margin-bottom: 15px; padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px; }', + '.reg-stat { text-align: center; min-width: 70px; }', + '.reg-stat-value { font-size: 20px; font-weight: 700; }', + '.reg-stat-value.gold { color: #f1c40f; }', + '.reg-stat-value.orange { color: #e67e22; }', + '.reg-stat-value.blue { color: #3498db; }', + '.reg-stat-value.purple { color: #9b59b6; }', + '.reg-stat-label { font-size: 10px; color: rgba(255,255,255,0.5); }', - '.toggle { position: relative; width: 44px; height: 24px; }', - '.toggle input { opacity: 0; width: 0; height: 0; }', - '.slider { position: absolute; cursor: pointer; inset: 0; background: rgba(255,255,255,0.1); border-radius: 12px; transition: 0.3s; }', - '.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: 0.3s; }', - 'input:checked + .slider { background: #667eea; }', - 'input:checked + .slider:before { transform: translateX(20px); }', + // Registry Table + '.registry-table { margin-bottom: 15px; }', + '.table-header { display: grid; grid-template-columns: 1fr 2fr 80px 60px; gap: 10px; padding: 8px 12px; background: rgba(0,0,0,0.3); border-radius: 6px 6px 0 0; font-size: 11px; color: rgba(255,255,255,0.5); text-transform: uppercase; }', + '.table-body { max-height: 180px; overflow-y: auto; }', + '.table-row { display: grid; grid-template-columns: 1fr 2fr 80px 60px; gap: 10px; padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); align-items: center; }', + '.url-cell { display: flex; align-items: center; gap: 6px; }', + '.short-url { color: #f1c40f; font-size: 13px; }', + '.copy-btn { background: none; border: none; cursor: pointer; opacity: 0.5; font-size: 12px; }', + '.copy-btn:hover { opacity: 1; }', + '.target-cell { font-size: 12px; color: rgba(255,255,255,0.6); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', + '.status-cell { display: flex; align-items: center; gap: 6px; font-size: 12px; }', + '.hits-cell { font-size: 12px; color: rgba(255,255,255,0.4); }', - '.view-select { padding: 6px 10px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: #e0e0e0; font-size: 12px; }', + // Registry Toggles + '.registry-toggles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 15px; }', + '.toggle-option { display: flex; align-items: center; gap: 8px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 6px; cursor: pointer; font-size: 13px; }', + '.toggle-option.static { cursor: default; }', + '.toggle-option code { font-size: 11px; }', - '.info-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 13px; }', - '.info-row:last-child { border-bottom: none; }', - 'code { padding: 2px 6px; background: rgba(0,0,0,0.3); border-radius: 4px; font-size: 12px; }', + // Services Registry + '.service-types-legend { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 15px; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.08); }', + '.type-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border: 1px solid; border-radius: 4px; font-size: 11px; }', + '.services-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }', + '@media (max-width: 600px) { .services-columns { grid-template-columns: 1fr; } }', + '.services-column { }', + '.column-title { display: flex; align-items: center; gap: 8px; margin: 0 0 12px 0; font-size: 13px; color: rgba(255,255,255,0.8); }', + '.column-title .count { margin-left: auto; font-size: 12px; }', + '.column-title .count.green { color: #2ecc71; }', + '.column-title .count.blue { color: #3498db; }', + '.services-list { display: flex; flex-direction: column; gap: 8px; max-height: 200px; overflow-y: auto; }', + '.service-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: rgba(0,0,0,0.2); border-radius: 6px; border-left: 3px solid; }', + '.svc-icon { font-size: 18px; }', + '.svc-info { flex: 1; min-width: 0; }', + '.svc-name { font-weight: 500; font-size: 13px; display: flex; align-items: center; gap: 6px; }', + '.svc-status-dot { width: 6px; height: 6px; border-radius: 50%; }', + '.svc-status-dot.online { background: #2ecc71; }', + '.svc-status-dot.offline { background: #e74c3c; }', + '.svc-detail { font-size: 11px; color: rgba(255,255,255,0.5); }', + '.svc-action { background: none; border: none; cursor: pointer; font-size: 14px; opacity: 0.7; }', + '.svc-action:hover { opacity: 1; }', - '.matrix-view { height: 200px; display: flex; align-items: center; justify-content: center; position: relative; }', + // Matrix + '.matrix-view { height: 180px; display: flex; align-items: center; justify-content: center; position: relative; }', '.matrix-center { z-index: 2; }', '.matrix-ring { position: absolute; width: 100%; height: 100%; }', - '.node { display: flex; flex-direction: column; align-items: center; font-size: 12px; }', + '.node { display: flex; flex-direction: column; align-items: center; font-size: 11px; }', '.node.master { background: linear-gradient(135deg, #667eea, #764ba2); padding: 12px 16px; border-radius: 12px; }', '.node.peer { position: absolute; top: 50%; left: 50%; width: 36px; height: 36px; margin: -18px; background: rgba(16,185,129,0.2); border: 2px solid #10b981; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; }', '.node.peer.offline { background: rgba(239,68,68,0.2); border-color: #ef4444; }', - '.services-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 10px; }', - '.service-card { padding: 12px; background: rgba(0,0,0,0.2); border-radius: 10px; text-align: center; }', - '.service-card .svc-icon { font-size: 24px; margin-bottom: 6px; }', - '.service-card .svc-name { font-size: 11px; font-weight: 500; }', - '.service-card .svc-status { font-size: 10px; margin-top: 4px; }', - '.svc-status.running { color: #10b981; }', + // Master Control + '.control-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 15px; }', + '.control-stat { text-align: center; padding: 15px; background: rgba(0,0,0,0.2); border-radius: 10px; }', + '.cs-value { font-size: 28px; font-weight: 700; }', + '.cs-label { font-size: 11px; color: #888; margin-top: 4px; }', - '.services-list .service-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }', - '.services-list .svc-name { flex: 1; }', - '.services-list .svc-port { color: #888; font-size: 12px; }', - '.services-list .svc-status { padding: 3px 8px; border-radius: 10px; font-size: 10px; }', - '.services-list .svc-status.running { background: rgba(16,185,129,0.2); color: #10b981; }', + // Mesh Stack + '.mesh-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }', + '@media (max-width: 900px) { .mesh-cards { grid-template-columns: 1fr; } }', + '.mesh-card { background: rgba(0,0,0,0.2); border-radius: 10px; padding: 15px; }', + '.mesh-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 13px; font-weight: 500; }', + '.mesh-card-body { }', + '.mesh-info { display: flex; justify-content: space-between; padding: 6px 0; font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.05); }', + '.mesh-info:last-child { border-bottom: none; }', + '.mesh-info code { padding: 2px 6px; background: rgba(0,0,0,0.3); border-radius: 4px; }', - '.peers-list { max-height: 300px; overflow-y: auto; }', + // Toggle Switch + '.toggle-switch { position: relative; width: 40px; height: 22px; }', + '.toggle-switch input { opacity: 0; width: 0; height: 0; }', + '.toggle-switch .slider { position: absolute; cursor: pointer; inset: 0; background: rgba(255,255,255,0.1); border-radius: 11px; transition: 0.3s; }', + '.toggle-switch .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: 0.3s; }', + '.toggle-switch input:checked + .slider { background: #667eea; }', + '.toggle-switch input:checked + .slider:before { transform: translateX(18px); }', + + // Peers + '.peers-list { max-height: 250px; overflow-y: auto; }', '.peer-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }', '.peer-icon { font-size: 20px; }', '.peer-info { flex: 1; }', - '.peer-name { font-weight: 500; }', + '.peer-name { font-weight: 500; font-size: 13px; }', '.peer-addr { font-size: 11px; color: #888; }', '.peer-status { width: 10px; height: 10px; border-radius: 50%; }', '.peer-status.online { background: #10b981; }', '.peer-status.offline { background: #ef4444; }', + '.btn-icon { background: none; border: none; cursor: pointer; opacity: 0.5; font-size: 14px; }', + '.btn-icon:hover { opacity: 1; }', - '.overview-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }', - '.ov-stat { text-align: center; padding: 15px; background: rgba(0,0,0,0.2); border-radius: 10px; }', - '.ov-value { font-size: 28px; font-weight: 700; }', - '.ov-label { font-size: 11px; color: #888; margin-top: 4px; }', + // Health + '.health-status { text-align: center; padding: 20px; }', + '.health-indicator { font-size: 48px; margin-bottom: 8px; }', + '.health-indicator.healthy { color: #2ecc71; }', + '.health-indicator.unhealthy { color: #e74c3c; }', + '.health-indicator.unknown { color: #95a5a6; }', + '.health-label { font-size: 14px; font-weight: 500; }', + '.health-checks { display: flex; justify-content: center; gap: 20px; margin: 15px 0; }', + '.check-item { font-size: 12px; }', + '.check-item .ok { color: #2ecc71; }', + '.check-item .off { color: #95a5a6; }', - '.config-grid { display: flex; flex-direction: column; gap: 8px; }', - '.on { color: #10b981; }', - '.off { color: #6b7280; }', - '.empty { text-align: center; padding: 30px; color: #666; }', + // Buttons + '.btn { padding: 8px 16px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #e0e0e0; cursor: pointer; font-size: 12px; transition: all 0.2s; }', + '.btn:hover { background: rgba(99,102,241,0.15); border-color: rgba(99,102,241,0.4); }', + '.btn.primary { background: linear-gradient(135deg, #667eea, #764ba2); border: none; }', + '.btn.small { padding: 6px 12px; font-size: 11px; }', - '.master-panel { grid-column: span 2; }', - '@media (max-width: 768px) { .master-panel { grid-column: span 1; } }' + // Empty state + '.empty-state { text-align: center; padding: 30px; color: rgba(255,255,255,0.4); font-size: 13px; }', + + // Modal + '.modal-form { margin-bottom: 20px; }', + '.form-group { margin-bottom: 15px; }', + '.form-group label { display: block; margin-bottom: 6px; font-size: 13px; color: rgba(255,255,255,0.8); }', + '.form-input, .form-select { width: 100%; padding: 10px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; color: #fff; font-size: 13px; }', + '.input-group { display: flex; align-items: center; }', + '.input-prefix { padding: 10px; background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.2); border-right: none; border-radius: 6px 0 0 6px; color: rgba(255,255,255,0.5); font-size: 12px; }', + '.input-group .form-input { border-radius: 0 6px 6px 0; }', + '.zone-preview { padding: 12px; background: rgba(0,0,0,0.4); border-radius: 6px; font-size: 11px; font-family: monospace; overflow-x: auto; white-space: pre; }', + '.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }' ].join('\n'); },