secubox-openwrt/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js
CyberMind-FR ef914b0b9f feat(p2p): Add SecuBox P2P Hub packages and Services Registry
- Add secubox-p2p backend package:
  - UCI configuration for P2P settings, DNS federation, WireGuard mesh, HAProxy
  - RPCD handler for peer management, service discovery, mesh configuration
  - Init script and main P2P manager daemon

- Add luci-app-secubox-p2p frontend package:
  - Main hub view with master control, network matrix visualization
  - Peers management with discovery and manual add
  - Services view showing local and shared services
  - Mesh network configuration (DNS, WireGuard, HAProxy)
  - Settings for P2P and registry configuration

- Add Services Registry view to luci-app-secubox
- Add listProfiles/applyProfile to secubox-admin API
- Fix P2P ACL permissions
- Remove old hub.js from luci-app-secubox (moved to dedicated package)

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

622 lines
25 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: [],
dnsConfig: {},
wgConfig: {},
haConfig: {},
registry: {},
health: {},
// View state
masterViewMode: 'master',
masterViewTab: 'overview',
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.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.dnsConfig = results[3] || {};
self.wgConfig = results[4] || {};
self.haConfig = results[5] || {};
self.registry = results[6] || {};
self.health = results[7] || {};
return {};
});
},
render: function() {
var self = this;
var container = E('div', { 'class': 'p2p-hub-page' }, [
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')
]),
// 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')
]),
// Main Content Grid
E('div', { 'class': 'hub-grid' }, [
// Master/Peer Overview
this.renderMasterPanel(),
// Network Matrix
this.renderNetworkMatrix(),
// DNS Federation
this.renderDNSPanel(),
// WireGuard Mesh
this.renderWireGuardPanel(),
// HAProxy
this.renderHAProxyPanel(),
// Hub Registry
this.renderRegistryPanel(),
// Services
this.renderServicesPanel(),
// Peers List
this.renderPeersPanel()
])
]);
// Auto-refresh
poll.add(function() {
self.refreshData();
}, 30);
return container;
},
refreshData: function() {
var self = this;
return Promise.all([
P2PAPI.getPeers(),
P2PAPI.healthCheck()
]).then(function(results) {
self.peers = results[0].peers || [];
self.health = results[1] || {};
}).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() {
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']),
E('select', {
'class': 'view-select',
'change': function(e) { self.switchView(e.target.value); }
}, viewNodes.map(function(n) {
return E('option', { 'value': n.id }, n.icon + ' ' + n.name);
}))
]),
// Tabs
E('div', { 'class': 'panel-tabs' }, [
this.renderTab('overview', '📊', 'Overview'),
this.renderTab('services', '📡', 'Services'),
this.renderTab('dns', '🌐', 'DNS'),
this.renderTab('config', '⚙️', 'Config')
]),
// Content
E('div', { 'id': 'master-content', 'class': 'panel-content' },
this.renderTabContent(this.masterViewTab)),
// 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')
])
]);
},
renderTab: function(id, icon, label) {
var self = this;
return E('button', {
'class': 'tab' + (this.masterViewTab === id ? ' active' : ''),
'click': function() { self.switchTab(id); }
}, [icon + ' ' + label]);
},
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')
)
]);
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')
])
])
]);
}
},
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));
});
},
switchView: function(viewId) {
this.masterViewMode = viewId;
ui.addNotification(null, E('p', 'Switched to view: ' + viewId), 'info');
},
renderNetworkMatrix: function() {
var self = this;
return E('div', { 'class': 'hub-panel matrix-panel' }, [
E('div', { 'class': 'panel-header' }, [
E('h3', {}, ['🕸️ ', '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
}, '🖥️');
})
)
])
]);
},
renderDNSPanel: 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' })
])
]),
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': '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))])
])
]);
},
renderHAProxyPanel: 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' })
])
]),
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')])
])
]);
},
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)
]);
})
)
]);
},
renderPeersPanel: function() {
var self = this;
return E('div', { 'class': 'hub-panel peers-panel' }, [
E('div', { 'class': 'panel-header' }, [
E('h3', {}, ['👥 ', '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' }, 'No peers connected. Click Discover to find peers.')
),
E('div', { 'class': 'panel-actions' }, [
E('button', { 'class': 'btn', 'click': function() { self.showAddPeerModal(); } }, ' Add Peer')
])
]);
},
// Actions
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(result) {
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(result) {
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) {
P2PAPI.setDNSConfig({ enabled: enabled }).then(function() {
ui.addNotification(null, E('p', 'DNS Federation ' + (enabled ? 'enabled' : 'disabled')), 'info');
});
},
toggleWireGuard: function(enabled) {
P2PAPI.setWireGuardConfig({ enabled: enabled }).then(function() {
ui.addNotification(null, E('p', 'WireGuard ' + (enabled ? 'enabled' : 'disabled')), 'info');
});
},
toggleHAProxy: function(enabled) {
P2PAPI.setHAProxyConfig({ enabled: enabled }).then(function() {
ui.addNotification(null, E('p', 'HAProxy ' + (enabled ? 'enabled' : 'disabled')), 'info');
});
},
showBroadcastModal: function() {
var self = this;
ui.showModal('Broadcast Command', [
E('select', { 'id': 'broadcast-cmd', 'class': 'cbi-input-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('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', {}, [
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': '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 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')
])
]);
},
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')
])
]);
},
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; }',
'.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; }',
'.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; }',
'.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; }',
'.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; }',
'.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; }',
'.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; }',
'.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); }',
'.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; }',
'.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; }',
'.matrix-view { height: 200px; 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.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; }',
'.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; }',
'.peers-list { max-height: 300px; 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-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; }',
'.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; }',
'.config-grid { display: flex; flex-direction: column; gap: 8px; }',
'.on { color: #10b981; }',
'.off { color: #6b7280; }',
'.empty { text-align: center; padding: 30px; color: #666; }',
'.master-panel { grid-column: span 2; }',
'@media (max-width: 768px) { .master-panel { grid-column: span 1; } }'
].join('\n');
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});