secubox-openwrt/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js
CyberMind-FR 4ab61b96e0 feat(p2p): Add globe visualization with peer map
- Animated 3D globe with CSS transformations
- Peer nodes positioned around globe with depth sorting
- Master node at center with pulse animation
- Connection lines between master and peers
- Stars background with twinkle animation
- Health indicators with emoji status (💚💛❤️)
- Quick stats (Peers, Online, Services, Registry)
- Quick actions (Discover, Sync All, Add Peer)
- Responsive layout for mobile

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 11:11:43 +01:00

1337 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require ui';
'require dom';
'require secubox-p2p/api as P2PAPI';
'require poll';
return view.extend({
// State
peers: [],
settings: {},
services: [],
sharedServices: [],
dnsConfig: {},
wgConfig: {},
haConfig: {},
registry: {},
health: {},
// View state
masterViewMode: 'master',
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;
return Promise.all([
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 {}; }),
P2PAPI.getRegistry().catch(function() { return {}; }),
P2PAPI.healthCheck().catch(function() { return {}; })
]).then(function(results) {
self.peers = results[0].peers || [];
self.settings = results[1] || {};
self.services = results[2].services || [];
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 {};
});
},
render: function() {
var self = this;
var container = E('div', { 'class': 'p2p-hub-master' }, [
E('style', {}, this.getStyles()),
// Globe Hero Section
this.renderGlobeHero(),
// Quick Stats Bar
this.renderQuickStats(),
// Main Panels Grid - Extensible
E('div', { 'class': 'hub-panels-grid', 'id': 'hub-panels' }, [
// Row 1: Hub Registry + Services Registry
this.renderHubRegistryPanel(),
this.renderServicesRegistryPanel(),
// Row 2: Network Matrix + Master Control
this.renderNetworkMatrix(),
this.renderMasterControlPanel(),
// Row 3: Mesh Stack (DNS + WG + HAProxy)
this.renderMeshStackPanel(),
// Row 4: Peers + Health
this.renderPeersPanel(),
this.renderHealthPanel()
])
]);
// Auto-refresh
poll.add(function() {
self.refreshData();
}, 30);
return container;
},
refreshData: function() {
var self = this;
return Promise.all([
P2PAPI.getPeers(),
P2PAPI.getServices(),
P2PAPI.getSharedServices(),
P2PAPI.healthCheck()
]).then(function(results) {
self.peers = results[0].peers || [];
self.services = results[1].services || [];
self.sharedServices = results[2].shared_services || [];
self.health = results[3] || {};
}).catch(function() {});
},
// ==================== Globe Hero ====================
renderGlobeHero: function() {
var self = this;
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
var totalPeers = this.peers.length;
var healthStatus = this.health.status || 'unknown';
var healthEmoji = healthStatus === 'healthy' ? '💚' : healthStatus === 'unhealthy' ? '❤️' : '💛';
// Generate peer positions around globe (pseudo-geographic)
var peerNodes = this.peers.map(function(peer, i) {
// Distribute peers around the globe
var lat = (Math.random() - 0.5) * 140; // -70 to 70 degrees
var lon = (i / Math.max(totalPeers, 1)) * 360 - 180; // Spread around
return {
peer: peer,
lat: lat,
lon: lon,
x: 50 + Math.cos(lon * Math.PI / 180) * Math.cos(lat * Math.PI / 180) * 40,
y: 50 + Math.sin(lat * Math.PI / 180) * 35,
z: Math.sin(lon * Math.PI / 180) * Math.cos(lat * Math.PI / 180)
};
}).sort(function(a, b) { return a.z - b.z; }); // Sort by depth
return E('div', { 'class': 'globe-hero' }, [
// Background stars
E('div', { 'class': 'stars-bg' }),
// Globe container
E('div', { 'class': 'globe-container' }, [
// Globe sphere
E('div', { 'class': 'globe' }, [
E('div', { 'class': 'globe-inner' }),
E('div', { 'class': 'globe-grid' }),
E('div', { 'class': 'globe-glow' })
]),
// Master node (center)
E('div', { 'class': 'globe-master-node' }, [
E('span', { 'class': 'master-icon' }, '👑'),
E('span', { 'class': 'master-pulse' })
]),
// Peer nodes on globe
E('div', { 'class': 'globe-peers' },
peerNodes.map(function(node) {
var opacity = 0.4 + (node.z + 1) * 0.3;
var scale = 0.6 + (node.z + 1) * 0.2;
return E('div', {
'class': 'globe-peer ' + (node.peer.status === 'online' ? 'online' : 'offline'),
'style': 'left: ' + node.x + '%; top: ' + node.y + '%; opacity: ' + opacity + '; transform: scale(' + scale + ');',
'title': (node.peer.name || node.peer.id) + ' - ' + (node.peer.address || 'Unknown')
}, [
E('span', { 'class': 'peer-dot' }),
E('span', { 'class': 'peer-label' }, node.peer.name || node.peer.id)
]);
})
),
// Connection lines (animated)
E('svg', { 'class': 'globe-connections', 'viewBox': '0 0 100 100' },
peerNodes.filter(function(n) { return n.z > -0.3; }).map(function(node) {
return E('line', {
'class': 'connection-line ' + (node.peer.status === 'online' ? 'active' : ''),
'x1': '50', 'y1': '50',
'x2': String(node.x), 'y2': String(node.y)
});
})
)
]),
// Hero Info Panel
E('div', { 'class': 'globe-info' }, [
E('div', { 'class': 'globe-title' }, [
E('span', { 'class': 'globe-icon' }, '🌐'),
E('span', {}, 'SecuBox Global Network')
]),
E('div', { 'class': 'globe-subtitle' }, 'Distributed P2P Mesh • MaaS Federation'),
// Health indicators
E('div', { 'class': 'globe-health' }, [
E('div', { 'class': 'health-item' }, [
E('span', { 'class': 'health-emoji' }, healthEmoji),
E('span', {}, 'System'),
E('span', { 'class': 'health-status ' + healthStatus }, healthStatus)
]),
E('div', { 'class': 'health-item' }, [
E('span', { 'class': 'health-emoji' }, this.dnsConfig.enabled ? '🌐' : '⚫'),
E('span', {}, 'DNS'),
E('span', { 'class': 'health-status ' + (this.dnsConfig.enabled ? 'healthy' : 'off') }, this.dnsConfig.enabled ? 'ON' : 'OFF')
]),
E('div', { 'class': 'health-item' }, [
E('span', { 'class': 'health-emoji' }, this.wgConfig.enabled ? '🔒' : '🔓'),
E('span', {}, 'WG'),
E('span', { 'class': 'health-status ' + (this.wgConfig.enabled ? 'healthy' : 'off') }, this.wgConfig.enabled ? 'ON' : 'OFF')
]),
E('div', { 'class': 'health-item' }, [
E('span', { 'class': 'health-emoji' }, this.haConfig.enabled ? '⚖️' : '⚫'),
E('span', {}, 'LB'),
E('span', { 'class': 'health-status ' + (this.haConfig.enabled ? 'healthy' : 'off') }, this.haConfig.enabled ? 'ON' : 'OFF')
])
]),
// Network stats
E('div', { 'class': 'globe-stats' }, [
E('div', { 'class': 'globe-stat' }, [
E('div', { 'class': 'gs-value' }, String(totalPeers)),
E('div', { 'class': 'gs-label' }, 'Peers')
]),
E('div', { 'class': 'globe-stat online' }, [
E('div', { 'class': 'gs-value' }, String(onlinePeers)),
E('div', { 'class': 'gs-label' }, 'Online')
]),
E('div', { 'class': 'globe-stat' }, [
E('div', { 'class': 'gs-value' }, String(this.services.length)),
E('div', { 'class': 'gs-label' }, 'Services')
]),
E('div', { 'class': 'globe-stat' }, [
E('div', { 'class': 'gs-value' }, String(this.getRegisteredServices().length)),
E('div', { 'class': 'gs-label' }, 'Registry')
])
]),
// Quick actions
E('div', { 'class': 'globe-actions' }, [
E('button', { 'class': 'globe-btn primary', 'click': function() { self.discoverPeers(); } }, '🔍 Discover'),
E('button', { 'class': 'globe-btn', 'click': function() { self.syncAll(); } }, '🔄 Sync All'),
E('button', { 'class': 'globe-btn', 'click': function() { self.showAddPeerModal(); } }, ' Add Peer')
])
])
]);
},
// ==================== 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-header' }, [
E('div', { 'class': 'hub-title-row' }, [
E('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);
}))
])
]);
},
// ==================== 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;
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)
]);
},
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
])
]);
},
// ==================== Hub Registry Panel ====================
renderHubRegistryPanel: function() {
var self = this;
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')
])
]);
},
renderRegistryEntry: function(service) {
var self = this;
var statusClass = service.status === 'active' ? 'active' : service.status === 'cached' ? 'cached' : 'error';
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))
]);
},
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;
},
// ==================== 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': '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' }, [
E('div', { 'class': 'matrix-center' }, [
E('div', { 'class': 'node master' }, ['👑', E('br', {}), 'Master'])
]),
E('div', { 'class': 'matrix-ring' },
this.peers.slice(0, 8).map(function(p, i) {
var angle = (i / Math.min(self.peers.length, 8)) * 360;
return E('div', {
'class': 'node peer ' + (p.status === 'online' ? 'online' : 'offline'),
'style': 'transform: rotate(' + angle + 'deg) translateX(70px) rotate(-' + angle + 'deg);',
'title': p.name || p.id
}, '🖥️');
})
)
])
]);
},
// ==================== Master Control ====================
renderMasterControlPanel: function() {
var self = this;
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': '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-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')
])
]);
},
// ==================== Mesh Stack ====================
renderMeshStackPanel: function() {
var self = this;
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': '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))])
])
])
])
]);
},
// ==================== Peers Panel ====================
renderPeersPanel: function() {
var self = this;
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' },
this.peers.length > 0 ?
this.peers.map(function(p) {
return E('div', { 'class': 'peer-row' }, [
E('span', { 'class': 'peer-icon' }, '🖥️'),
E('div', { 'class': 'peer-info' }, [
E('div', { 'class': 'peer-name' }, p.name || p.id),
E('div', { 'class': 'peer-addr' }, p.address || 'Unknown')
]),
E('span', { 'class': 'peer-status ' + (p.status === 'online' ? 'online' : 'offline') }),
E('button', { 'class': 'btn-icon', 'click': function() { self.removePeer(p.id); } }, '✕')
]);
}) :
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')
])
]);
},
// ==================== 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');
});
},
broadcastRestart: function() {
P2PAPI.broadcastCommand('restart').then(function() {
ui.addNotification(null, E('p', 'Restart broadcast sent'), 'info');
});
},
discoverPeers: function() {
var self = this;
ui.addNotification(null, E('p', 'Discovering peers...'), 'info');
P2PAPI.discover(5).then(function() {
ui.addNotification(null, E('p', 'Discovery complete'), 'info');
self.refreshData();
});
},
removePeer: function(peerId) {
var self = this;
P2PAPI.removePeer(peerId).then(function() {
ui.addNotification(null, E('p', 'Peer removed'), 'info');
self.refreshData();
});
},
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');
});
},
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': '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': '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;
P2PAPI.broadcastCommand(cmd);
ui.hideModal();
ui.addNotification(null, E('p', 'Command "' + cmd + '" broadcast'), 'info');
} }, 'Broadcast')
])
]);
},
showAddPeerModal: function() {
var self = this;
ui.showModal('Add 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': '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;
var name = document.getElementById('peer-name').value;
if (addr) {
P2PAPI.addPeer(addr, name).then(function() {
ui.hideModal();
ui.addNotification(null, E('p', 'Peer added'), 'info');
self.refreshData();
});
}
} }, 'Add')
])
]);
},
// ==================== Styles ====================
getStyles: function() {
return [
// Base
'.p2p-hub-master { background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%); min-height: 100vh; padding: 20px; color: #e0e0e0; }',
// Globe Hero
'.globe-hero { display: flex; align-items: center; justify-content: center; gap: 60px; padding: 40px 20px; margin-bottom: 30px; position: relative; overflow: hidden; background: radial-gradient(ellipse at center, rgba(102,126,234,0.1) 0%, transparent 70%); border-radius: 20px; border: 1px solid rgba(255,255,255,0.05); min-height: 350px; }',
'@media (max-width: 900px) { .globe-hero { flex-direction: column; gap: 30px; padding: 30px 15px; } }',
// Stars background
'.stars-bg { position: absolute; inset: 0; background-image: radial-gradient(2px 2px at 20px 30px, rgba(255,255,255,0.3), transparent), radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.2), transparent), radial-gradient(1px 1px at 90px 40px, rgba(255,255,255,0.4), transparent), radial-gradient(2px 2px at 130px 80px, rgba(255,255,255,0.2), transparent), radial-gradient(1px 1px at 160px 120px, rgba(255,255,255,0.3), transparent), radial-gradient(2px 2px at 200px 50px, rgba(255,255,255,0.2), transparent), radial-gradient(1px 1px at 250px 100px, rgba(255,255,255,0.4), transparent), radial-gradient(2px 2px at 300px 60px, rgba(255,255,255,0.2), transparent); background-repeat: repeat; background-size: 350px 150px; animation: twinkle 4s ease-in-out infinite; }',
'@keyframes twinkle { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }',
// Globe container
'.globe-container { position: relative; width: 280px; height: 280px; flex-shrink: 0; }',
// Globe sphere
'.globe { position: absolute; inset: 20px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, rgba(102,126,234,0.4), rgba(118,75,162,0.2) 50%, rgba(10,10,15,0.8)); box-shadow: inset -20px -20px 40px rgba(0,0,0,0.6), inset 10px 10px 30px rgba(102,126,234,0.3), 0 0 60px rgba(102,126,234,0.3); animation: globe-rotate 30s linear infinite; overflow: hidden; }',
'@keyframes globe-rotate { from { transform: rotateY(0deg); } to { transform: rotateY(360deg); } }',
'.globe-inner { position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle at 25% 25%, rgba(255,255,255,0.1), transparent 50%); }',
'.globe-grid { position: absolute; inset: 0; border-radius: 50%; background: repeating-linear-gradient(0deg, transparent, transparent 18px, rgba(102,126,234,0.15) 18px, rgba(102,126,234,0.15) 20px), repeating-linear-gradient(90deg, transparent, transparent 18px, rgba(102,126,234,0.15) 18px, rgba(102,126,234,0.15) 20px); opacity: 0.5; }',
'.globe-glow { position: absolute; inset: -10px; border-radius: 50%; background: radial-gradient(circle, transparent 60%, rgba(102,126,234,0.2) 80%, transparent); animation: pulse-glow 3s ease-in-out infinite; }',
'@keyframes pulse-glow { 0%, 100% { opacity: 0.5; transform: scale(1); } 50% { opacity: 1; transform: scale(1.05); } }',
// Master node
'.globe-master-node { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; }',
'.master-icon { font-size: 32px; display: block; filter: drop-shadow(0 0 10px rgba(241,196,15,0.8)); animation: master-float 3s ease-in-out infinite; }',
'@keyframes master-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }',
'.master-pulse { position: absolute; inset: -15px; border-radius: 50%; border: 2px solid rgba(241,196,15,0.5); animation: master-pulse 2s ease-out infinite; }',
'@keyframes master-pulse { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(2); opacity: 0; } }',
// Peer nodes on globe
'.globe-peers { position: absolute; inset: 0; }',
'.globe-peer { position: absolute; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; gap: 4px; transition: all 0.3s; cursor: pointer; }',
'.globe-peer:hover { transform: translate(-50%, -50%) scale(1.2); z-index: 20; }',
'.peer-dot { width: 12px; height: 12px; border-radius: 50%; background: #10b981; box-shadow: 0 0 10px rgba(16,185,129,0.6); }',
'.globe-peer.offline .peer-dot { background: #ef4444; box-shadow: 0 0 10px rgba(239,68,68,0.6); }',
'.peer-label { font-size: 9px; color: rgba(255,255,255,0.7); white-space: nowrap; background: rgba(0,0,0,0.5); padding: 2px 6px; border-radius: 4px; opacity: 0; transition: opacity 0.3s; }',
'.globe-peer:hover .peer-label { opacity: 1; }',
// Connection lines
'.globe-connections { position: absolute; inset: 0; pointer-events: none; }',
'.connection-line { stroke: rgba(102,126,234,0.2); stroke-width: 0.5; stroke-dasharray: 4 2; }',
'.connection-line.active { stroke: rgba(16,185,129,0.4); animation: line-flow 2s linear infinite; }',
'@keyframes line-flow { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -20; } }',
// Globe info panel
'.globe-info { max-width: 400px; }',
'.globe-title { display: flex; align-items: center; gap: 12px; font-size: 28px; font-weight: 700; margin-bottom: 8px; }',
'.globe-icon { font-size: 36px; animation: globe-icon-spin 10s linear infinite; }',
'@keyframes globe-icon-spin { from { transform: rotateY(0deg); } to { transform: rotateY(360deg); } }',
'.globe-subtitle { color: #888; font-size: 14px; margin-bottom: 20px; }',
// Health indicators
'.globe-health { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; }',
'.health-item { display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: rgba(0,0,0,0.3); border-radius: 8px; font-size: 12px; }',
'.health-emoji { font-size: 16px; }',
'.health-status { font-weight: 600; margin-left: 4px; }',
'.health-status.healthy { color: #2ecc71; }',
'.health-status.unhealthy { color: #e74c3c; }',
'.health-status.unknown { color: #f39c12; }',
'.health-status.off { color: #95a5a6; }',
// Globe stats
'.globe-stats { display: flex; gap: 20px; margin-bottom: 20px; }',
'.globe-stat { text-align: center; padding: 15px 20px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; }',
'.globe-stat.online { border-color: rgba(16,185,129,0.3); background: rgba(16,185,129,0.1); }',
'.gs-value { font-size: 28px; font-weight: 700; }',
'.globe-stat.online .gs-value { color: #10b981; }',
'.gs-label { font-size: 11px; color: #888; margin-top: 4px; }',
// Globe actions
'.globe-actions { display: flex; gap: 10px; flex-wrap: wrap; }',
'.globe-btn { padding: 10px 20px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; color: #e0e0e0; cursor: pointer; font-size: 13px; transition: all 0.3s; }',
'.globe-btn:hover { background: rgba(102,126,234,0.2); border-color: rgba(102,126,234,0.4); transform: translateY(-2px); }',
'.globe-btn.primary { background: linear-gradient(135deg, #667eea, #764ba2); border: none; }',
// 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; }',
// 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.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; }',
// 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; }',
// 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; }',
// 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); }',
// 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); }',
// 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; }',
// 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
'.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: 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; }',
// 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; }',
// 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; }',
// 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; 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; }',
// 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; }',
// 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; }',
// 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');
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});