feat(p2p): Add master deployment, DNS bridge, and WireGuard mirror
- Add renderServiceItemWithDeploy() for services with mesh deployment status - Add deployment action handlers: deployAllRegistry, deployRegistryEntry, deployAllServices, deployLocalServices, pullAllServices, deployService, pullService - Add deployment modals: showDeployRegistryModal, showDeployServicesModal - Add DNS Bridge with load balancing strategies (round-robin, weighted, geo, latency) - Add Onion Relay integration for privacy/fallback routing - Add WireGuard Mirror inverse system with modes (active-passive, active-active, ring, full-mesh) - Add showDNSBridgeModal and showWGMirrorModal configuration dialogs - Add new API functions for deployment, DNS bridge, and WG mirror - Add comprehensive CSS styles for deploy banners, mesh status dots, modal styles Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4ab61b96e0
commit
afd761a239
@ -132,6 +132,87 @@ var callHealthCheck = rpc.declare({
|
||||
expect: {}
|
||||
});
|
||||
|
||||
// Deployment - Registry
|
||||
var callDeployRegistry = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'deploy_registry',
|
||||
expect: { success: false, deployed_peers: 0 }
|
||||
});
|
||||
|
||||
var callDeployRegistryEntry = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'deploy_registry_entry',
|
||||
params: ['short_url'],
|
||||
expect: { success: false, deployed_peers: 0 }
|
||||
});
|
||||
|
||||
// Deployment - Services
|
||||
var callDeployServices = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'deploy_services',
|
||||
expect: { success: false, services_deployed: 0, deployed_peers: 0 }
|
||||
});
|
||||
|
||||
var callDeployLocalServices = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'deploy_local_services',
|
||||
expect: { success: false, deployed_peers: 0 }
|
||||
});
|
||||
|
||||
var callDeployService = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'deploy_service',
|
||||
params: ['service_id'],
|
||||
expect: { success: false, deployed_peers: 0 }
|
||||
});
|
||||
|
||||
var callPullMeshServices = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'pull_mesh_services',
|
||||
expect: { success: false, services_pulled: 0 }
|
||||
});
|
||||
|
||||
var callPullService = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'pull_service',
|
||||
params: ['service_id', 'peer_id'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
// DNS Bridge
|
||||
var callGetDNSBridgeConfig = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'get_dns_bridge_config',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callSetDNSBridgeConfig = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'set_dns_bridge_config',
|
||||
params: ['config'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
// WireGuard Mirror
|
||||
var callGetWGMirrorConfig = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'get_wg_mirror_config',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callSetWGMirrorConfig = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'set_wg_mirror_config',
|
||||
params: ['config'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callSyncWGMirror = rpc.declare({
|
||||
object: 'luci.secubox-p2p',
|
||||
method: 'sync_wg_mirror',
|
||||
expect: { success: false, synced_peers: 0 }
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
// Peers
|
||||
getPeers: function() { return callGetPeers(); },
|
||||
@ -168,5 +249,25 @@ return baseclass.extend({
|
||||
registerURL: function(shortUrl, targetUrl) { return callRegisterURL(shortUrl, targetUrl); },
|
||||
|
||||
// Health
|
||||
healthCheck: function() { return callHealthCheck(); }
|
||||
healthCheck: function() { return callHealthCheck(); },
|
||||
|
||||
// Deployment - Registry
|
||||
deployRegistry: function() { return callDeployRegistry(); },
|
||||
deployRegistryEntry: function(shortUrl) { return callDeployRegistryEntry(shortUrl); },
|
||||
|
||||
// Deployment - Services
|
||||
deployServices: function() { return callDeployServices(); },
|
||||
deployLocalServices: function() { return callDeployLocalServices(); },
|
||||
deployService: function(serviceId) { return callDeployService(serviceId); },
|
||||
pullMeshServices: function() { return callPullMeshServices(); },
|
||||
pullService: function(serviceId, peerId) { return callPullService(serviceId, peerId); },
|
||||
|
||||
// DNS Bridge
|
||||
getDNSBridgeConfig: function() { return callGetDNSBridgeConfig(); },
|
||||
setDNSBridgeConfig: function(config) { return callSetDNSBridgeConfig(config); },
|
||||
|
||||
// WireGuard Mirror
|
||||
getWGMirrorConfig: function() { return callGetWGMirrorConfig(); },
|
||||
setWGMirrorConfig: function(config) { return callSetWGMirrorConfig(config); },
|
||||
syncWGMirror: function() { return callSyncWGMirror(); }
|
||||
});
|
||||
|
||||
@ -51,6 +51,26 @@ return view.extend({
|
||||
autoRegister: false
|
||||
},
|
||||
|
||||
// DNS Bridge Config
|
||||
dnsBridgeConfig: {
|
||||
enabled: false,
|
||||
strategy: 'round-robin',
|
||||
onionEnabled: false,
|
||||
meshSync: true,
|
||||
upstreamDNS: ['1.1.1.1', '8.8.8.8']
|
||||
},
|
||||
|
||||
// WireGuard Mirror Config
|
||||
wgMirrorConfig: {
|
||||
enabled: false,
|
||||
mode: 'active-passive',
|
||||
syncInterval: 30,
|
||||
keyRotation: true,
|
||||
peerMirroring: true,
|
||||
autoReconnect: true,
|
||||
inverseTunnel: false
|
||||
},
|
||||
|
||||
load: function() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
@ -320,6 +340,7 @@ return view.extend({
|
||||
var self = this;
|
||||
var registry = this.hubRegistry;
|
||||
var services = this.getRegisteredServices();
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
|
||||
return E('div', { 'class': 'panel hub-registry-panel' }, [
|
||||
E('div', { 'class': 'panel-header gold' }, [
|
||||
@ -334,37 +355,53 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
|
||||
// Master Deploy Banner
|
||||
E('div', { 'class': 'deploy-banner' }, [
|
||||
E('div', { 'class': 'deploy-info' }, [
|
||||
E('span', { 'class': 'deploy-icon' }, '🚀'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'deploy-title' }, 'Master Deployment'),
|
||||
E('div', { 'class': 'deploy-desc' }, onlinePeers + ' peers ready for distribution')
|
||||
])
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'btn deploy-btn',
|
||||
'click': function() { self.showDeployRegistryModal(); },
|
||||
'disabled': onlinePeers === 0
|
||||
}, '📤 Deploy to Mesh')
|
||||
]),
|
||||
|
||||
// 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-label' }, 'Entries')
|
||||
]),
|
||||
E('div', { 'class': 'reg-stat' }, [
|
||||
E('div', { 'class': 'reg-stat-value orange' }, String(this.peers.length)),
|
||||
E('div', { 'class': 'reg-stat-value green' }, String(services.filter(function(s) { return s.deployed; }).length || services.length)),
|
||||
E('div', { 'class': 'reg-stat-label' }, 'Deployed')
|
||||
]),
|
||||
E('div', { 'class': 'reg-stat' }, [
|
||||
E('div', { 'class': 'reg-stat-value orange' }, String(onlinePeers)),
|
||||
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
|
||||
// Short URL table with deploy status
|
||||
E('div', { 'class': 'registry-table' }, [
|
||||
E('div', { 'class': 'table-header' }, [
|
||||
E('span', {}, 'Short URL'),
|
||||
E('span', {}, 'Target'),
|
||||
E('span', {}, 'Status'),
|
||||
E('span', {}, 'Hits')
|
||||
E('span', {}, 'Mesh'),
|
||||
E('span', {}, 'Deploy')
|
||||
]),
|
||||
E('div', { 'class': 'table-body' },
|
||||
services.length > 0 ?
|
||||
services.map(function(svc) { return self.renderRegistryEntry(svc); }) :
|
||||
services.map(function(svc) { return self.renderRegistryEntryWithDeploy(svc); }) :
|
||||
E('div', { 'class': 'empty-state' }, 'No services registered')
|
||||
)
|
||||
]),
|
||||
@ -377,7 +414,7 @@ return view.extend({
|
||||
]),
|
||||
E('label', { 'class': 'toggle-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': this.maasConfig.autoRegister }),
|
||||
E('span', {}, '🔄'), E('span', {}, 'Auto-Register')
|
||||
E('span', {}, '🔄'), E('span', {}, 'Auto-Deploy')
|
||||
]),
|
||||
E('label', { 'class': 'toggle-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': registry.cacheEnabled, 'change': function(e) { self.toggleCache(e.target.checked); } }),
|
||||
@ -390,14 +427,41 @@ return view.extend({
|
||||
|
||||
// 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')
|
||||
E('button', { 'class': 'btn primary', 'click': function() { self.showRegisterURLModal(); } }, '➕ Register'),
|
||||
E('button', { 'class': 'btn', 'click': function() { self.deployAllRegistry(); } }, '🚀 Deploy All'),
|
||||
E('button', { 'class': 'btn', 'click': function() { self.syncRegistry(); } }, '🔄 Sync'),
|
||||
E('button', { 'class': 'btn', 'click': function() { self.showDNSConfigModal(); } }, '⚙️ DNS')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderRegistryEntryWithDeploy: function(service) {
|
||||
var self = this;
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
var deployedTo = service.deployedTo || 0;
|
||||
var deployStatus = deployedTo >= onlinePeers ? 'full' : deployedTo > 0 ? 'partial' : 'none';
|
||||
|
||||
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': 'mesh-status ' + deployStatus }, [
|
||||
E('span', { 'class': 'mesh-dots' }, [
|
||||
E('span', { 'class': 'dot ' + (deployedTo > 0 ? 'active' : '') }),
|
||||
E('span', { 'class': 'dot ' + (deployedTo > 1 ? 'active' : '') }),
|
||||
E('span', { 'class': 'dot ' + (deployedTo > 2 ? 'active' : '') })
|
||||
]),
|
||||
E('span', { 'class': 'mesh-count' }, deployedTo + '/' + onlinePeers)
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'deploy-entry-btn ' + (deployStatus === 'full' ? 'deployed' : ''),
|
||||
'click': function() { self.deployRegistryEntry(service); }
|
||||
}, deployStatus === 'full' ? '✓' : '📤')
|
||||
]);
|
||||
},
|
||||
|
||||
renderRegistryEntry: function(service) {
|
||||
var self = this;
|
||||
var statusClass = service.status === 'active' ? 'active' : service.status === 'cached' ? 'cached' : 'error';
|
||||
@ -450,14 +514,38 @@ return view.extend({
|
||||
var self = this;
|
||||
var localServices = this.getLocalServicesTyped();
|
||||
var networkServices = this.getNetworkServicesTyped();
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
|
||||
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('span', {}, 'Services Registry'),
|
||||
E('span', { 'class': 'badge' }, 'Distributed')
|
||||
]),
|
||||
E('button', { 'class': 'btn small', 'click': function() { self.refreshServicesRegistry(); } }, 'Refresh')
|
||||
E('button', { 'class': 'btn small', 'click': function() { self.refreshServicesRegistry(); } }, '🔄')
|
||||
]),
|
||||
|
||||
// Master Deploy Banner for Services
|
||||
E('div', { 'class': 'deploy-banner services' }, [
|
||||
E('div', { 'class': 'deploy-info' }, [
|
||||
E('span', { 'class': 'deploy-icon' }, '⚡'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'deploy-title' }, 'Service Distribution'),
|
||||
E('div', { 'class': 'deploy-desc' }, 'Deploy services across ' + onlinePeers + ' mesh nodes')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'deploy-actions-mini' }, [
|
||||
E('button', {
|
||||
'class': 'btn small',
|
||||
'click': function() { self.deployAllServices(); },
|
||||
'disabled': onlinePeers === 0
|
||||
}, '🚀 Deploy All'),
|
||||
E('button', {
|
||||
'class': 'btn small',
|
||||
'click': function() { self.showDeployServicesModal(); }
|
||||
}, '⚙️ Configure')
|
||||
])
|
||||
]),
|
||||
|
||||
// Service type legend
|
||||
@ -471,31 +559,33 @@ return view.extend({
|
||||
})
|
||||
),
|
||||
|
||||
// Two columns
|
||||
// Two columns with deploy controls
|
||||
E('div', { 'class': 'services-columns' }, [
|
||||
// Local Services
|
||||
// Local Services (Deployable)
|
||||
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('span', { 'class': 'count green' }, localServices.length + ' active'),
|
||||
E('button', { 'class': 'deploy-all-btn', 'click': function() { self.deployLocalServices(); }, 'title': 'Deploy all to mesh' }, '📤')
|
||||
]),
|
||||
E('div', { 'class': 'services-list' },
|
||||
localServices.length > 0 ?
|
||||
localServices.map(function(svc) { return self.renderServiceItem(svc, true); }) :
|
||||
localServices.map(function(svc) { return self.renderServiceItemWithDeploy(svc, true); }) :
|
||||
E('div', { 'class': 'empty-state' }, 'No services running')
|
||||
)
|
||||
]),
|
||||
// Network Services
|
||||
// Network Services (From Peers)
|
||||
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('span', {}, 'Mesh Services'),
|
||||
E('span', { 'class': 'count blue' }, networkServices.length + ' distributed'),
|
||||
E('button', { 'class': 'pull-all-btn', 'click': function() { self.pullAllServices(); }, 'title': 'Pull from mesh' }, '📥')
|
||||
]),
|
||||
E('div', { 'class': 'services-list' },
|
||||
networkServices.length > 0 ?
|
||||
networkServices.map(function(svc) { return self.renderServiceItem(svc, false); }) :
|
||||
networkServices.map(function(svc) { return self.renderServiceItemWithDeploy(svc, false); }) :
|
||||
E('div', { 'class': 'empty-state' }, 'No peer services found')
|
||||
)
|
||||
])
|
||||
@ -531,6 +621,47 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
renderServiceItemWithDeploy: function(service, isLocal) {
|
||||
var self = this;
|
||||
var type = this.serviceTypes[service.type] || { icon: '❓', name: service.type || 'unknown', color: '#95a5a6' };
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
var deployedTo = service.deployedTo || 0;
|
||||
var deployStatus = deployedTo >= onlinePeers ? 'full' : deployedTo > 0 ? 'partial' : 'none';
|
||||
|
||||
return E('div', { 'class': 'service-item with-deploy', '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'))
|
||||
]),
|
||||
// Mesh deployment status
|
||||
E('div', { 'class': 'svc-mesh-status ' + deployStatus }, [
|
||||
E('span', { 'class': 'mesh-micro-dots' }, [
|
||||
E('span', { 'class': 'micro-dot ' + (deployedTo > 0 ? 'active' : '') }),
|
||||
E('span', { 'class': 'micro-dot ' + (deployedTo > 1 ? 'active' : '') }),
|
||||
E('span', { 'class': 'micro-dot ' + (deployedTo > 2 ? 'active' : '') })
|
||||
]),
|
||||
E('span', { 'class': 'mesh-count-mini' }, deployedTo + '/' + onlinePeers)
|
||||
]),
|
||||
// Action button
|
||||
isLocal ?
|
||||
E('button', {
|
||||
'class': 'svc-deploy-btn ' + (deployStatus === 'full' ? 'deployed' : ''),
|
||||
'click': function() { self.deployService(service); },
|
||||
'title': deployStatus === 'full' ? 'Deployed to all peers' : 'Deploy to mesh'
|
||||
}, deployStatus === 'full' ? '✓' : '📤') :
|
||||
E('button', {
|
||||
'class': 'svc-pull-btn',
|
||||
'click': function() { self.pullService(service); },
|
||||
'title': 'Pull to local'
|
||||
}, '📥')
|
||||
]);
|
||||
},
|
||||
|
||||
getLocalServicesTyped: function() {
|
||||
var services = [];
|
||||
var self = this;
|
||||
@ -653,15 +784,19 @@ return view.extend({
|
||||
// ==================== Mesh Stack ====================
|
||||
renderMeshStackPanel: function() {
|
||||
var self = this;
|
||||
var dnsBridgeConfig = this.dnsBridgeConfig || { enabled: false, strategy: 'round-robin', onionEnabled: false };
|
||||
var wgMirrorConfig = this.wgMirrorConfig || { enabled: false, mode: 'active-passive', syncInterval: 30 };
|
||||
|
||||
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('span', {}, 'Mesh Infrastructure'),
|
||||
E('span', { 'class': 'badge' }, 'MaaS Stack')
|
||||
])
|
||||
]),
|
||||
|
||||
// Top row: Core Services
|
||||
E('div', { 'class': 'mesh-cards' }, [
|
||||
// DNS Federation
|
||||
E('div', { 'class': 'mesh-card' }, [
|
||||
@ -707,6 +842,238 @@ return view.extend({
|
||||
E('div', { 'class': 'mesh-info' }, ['Backends: ', E('strong', {}, String(this.peers.length + 1))])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Bottom row: Advanced Features
|
||||
E('h4', { 'class': 'mesh-section-title' }, '🔗 Advanced Mesh Features'),
|
||||
E('div', { 'class': 'mesh-cards advanced' }, [
|
||||
// DNS Bridge with Load Balancing
|
||||
E('div', { 'class': 'mesh-card featured' }, [
|
||||
E('div', { 'class': 'mesh-card-header' }, [
|
||||
E('span', {}, '🌉 DNS Bridge'),
|
||||
E('label', { 'class': 'toggle-switch' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': dnsBridgeConfig.enabled, 'change': function(e) { self.toggleDNSBridge(e.target.checked); } }),
|
||||
E('span', { 'class': 'slider' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mesh-card-body' }, [
|
||||
E('div', { 'class': 'mesh-info' }, ['LB Strategy: ', E('select', {
|
||||
'class': 'mini-select',
|
||||
'value': dnsBridgeConfig.strategy,
|
||||
'change': function(e) { self.setDNSBridgeStrategy(e.target.value); }
|
||||
}, [
|
||||
E('option', { 'value': 'round-robin' }, 'Round Robin'),
|
||||
E('option', { 'value': 'weighted' }, 'Weighted'),
|
||||
E('option', { 'value': 'geo' }, 'Geo-based'),
|
||||
E('option', { 'value': 'latency' }, 'Latency')
|
||||
])]),
|
||||
E('div', { 'class': 'mesh-info' }, ['Mesh Sync: ', E('span', { 'class': 'status-indicator active' }, '● Active')]),
|
||||
E('div', { 'class': 'mesh-info onion-row' }, [
|
||||
'Onion Relay: ',
|
||||
E('label', { 'class': 'toggle-switch mini' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': dnsBridgeConfig.onionEnabled, 'change': function(e) { self.toggleOnionRelay(e.target.checked); } }),
|
||||
E('span', { 'class': 'slider' })
|
||||
]),
|
||||
E('span', { 'class': 'onion-icon' }, '🧅')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mesh-card-actions' }, [
|
||||
E('button', { 'class': 'btn small', 'click': function() { self.showDNSBridgeModal(); } }, '⚙️ Configure')
|
||||
])
|
||||
]),
|
||||
|
||||
// WireGuard Mirror Inverse System
|
||||
E('div', { 'class': 'mesh-card featured' }, [
|
||||
E('div', { 'class': 'mesh-card-header' }, [
|
||||
E('span', {}, '🪞 WG Mirror'),
|
||||
E('label', { 'class': 'toggle-switch' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': wgMirrorConfig.enabled, 'change': function(e) { self.toggleWGMirror(e.target.checked); } }),
|
||||
E('span', { 'class': 'slider' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mesh-card-body' }, [
|
||||
E('div', { 'class': 'mesh-info' }, ['Mode: ', E('select', {
|
||||
'class': 'mini-select',
|
||||
'value': wgMirrorConfig.mode,
|
||||
'change': function(e) { self.setWGMirrorMode(e.target.value); }
|
||||
}, [
|
||||
E('option', { 'value': 'active-passive' }, 'Active-Passive'),
|
||||
E('option', { 'value': 'active-active' }, 'Active-Active'),
|
||||
E('option', { 'value': 'ring' }, 'Ring Topology'),
|
||||
E('option', { 'value': 'full-mesh' }, 'Full Mesh')
|
||||
])]),
|
||||
E('div', { 'class': 'mesh-info' }, ['Sync Interval: ', E('code', {}, wgMirrorConfig.syncInterval + 's')]),
|
||||
E('div', { 'class': 'mesh-info' }, ['Mirror Peers: ', E('strong', {}, String(this.peers.filter(function(p) { return p.wgMirror; }).length || this.peers.length))])
|
||||
]),
|
||||
E('div', { 'class': 'mesh-card-actions' }, [
|
||||
E('button', { 'class': 'btn small', 'click': function() { self.showWGMirrorModal(); } }, '⚙️ Configure'),
|
||||
E('button', { 'class': 'btn small', 'click': function() { self.syncWGMirror(); } }, '🔄 Sync')
|
||||
])
|
||||
]),
|
||||
|
||||
// Onion Relay (Tor Integration)
|
||||
E('div', { 'class': 'mesh-card' }, [
|
||||
E('div', { 'class': 'mesh-card-header' }, [
|
||||
E('span', {}, '🧅 Onion Relay'),
|
||||
E('label', { 'class': 'toggle-switch' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': dnsBridgeConfig.onionEnabled, 'change': function(e) { self.toggleOnionRelay(e.target.checked); } }),
|
||||
E('span', { 'class': 'slider' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'mesh-card-body' }, [
|
||||
E('div', { 'class': 'mesh-info' }, ['Hidden Service: ', E('code', {}, 'sb******.onion')]),
|
||||
E('div', { 'class': 'mesh-info' }, ['Relay Mode: ', E('strong', {}, 'Bridge')]),
|
||||
E('div', { 'class': 'mesh-info' }, ['Circuit: ', E('span', { 'class': 'status-indicator ' + (dnsBridgeConfig.onionEnabled ? 'active' : 'inactive') }, dnsBridgeConfig.onionEnabled ? '● Ready' : '○ Off')])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
// DNS Bridge and WG Mirror toggles
|
||||
toggleDNSBridge: function(enabled) {
|
||||
this.dnsBridgeConfig = this.dnsBridgeConfig || {};
|
||||
this.dnsBridgeConfig.enabled = enabled;
|
||||
ui.addNotification(null, E('p', 'DNS Bridge ' + (enabled ? 'enabled' : 'disabled')), 'info');
|
||||
},
|
||||
|
||||
setDNSBridgeStrategy: function(strategy) {
|
||||
this.dnsBridgeConfig = this.dnsBridgeConfig || {};
|
||||
this.dnsBridgeConfig.strategy = strategy;
|
||||
ui.addNotification(null, E('p', 'DNS Bridge strategy: ' + strategy), 'info');
|
||||
},
|
||||
|
||||
toggleOnionRelay: function(enabled) {
|
||||
this.dnsBridgeConfig = this.dnsBridgeConfig || {};
|
||||
this.dnsBridgeConfig.onionEnabled = enabled;
|
||||
ui.addNotification(null, E('p', 'Onion Relay ' + (enabled ? 'enabled' : 'disabled')), 'info');
|
||||
},
|
||||
|
||||
toggleWGMirror: function(enabled) {
|
||||
this.wgMirrorConfig = this.wgMirrorConfig || {};
|
||||
this.wgMirrorConfig.enabled = enabled;
|
||||
ui.addNotification(null, E('p', 'WireGuard Mirror ' + (enabled ? 'enabled' : 'disabled')), 'info');
|
||||
},
|
||||
|
||||
setWGMirrorMode: function(mode) {
|
||||
this.wgMirrorConfig = this.wgMirrorConfig || {};
|
||||
this.wgMirrorConfig.mode = mode;
|
||||
ui.addNotification(null, E('p', 'WireGuard Mirror mode: ' + mode), 'info');
|
||||
},
|
||||
|
||||
syncWGMirror: function() {
|
||||
ui.addNotification(null, E('p', '🔄 Syncing WireGuard mirror configurations...'), 'info');
|
||||
},
|
||||
|
||||
showDNSBridgeModal: function() {
|
||||
var self = this;
|
||||
var config = this.dnsBridgeConfig || {};
|
||||
|
||||
ui.showModal('DNS Bridge Configuration', [
|
||||
E('div', { 'class': 'modal-form' }, [
|
||||
E('div', { 'class': 'deploy-modal-header' }, [
|
||||
E('span', { 'class': 'deploy-modal-icon' }, '🌉'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'deploy-modal-title' }, 'DNS Bridge with Load Balancing'),
|
||||
E('div', { 'class': 'deploy-modal-subtitle' }, 'Synchronize DNS across mesh with intelligent load balancing')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Load Balancing Strategy'),
|
||||
E('select', { 'id': 'dns-lb-strategy', 'class': 'form-select' }, [
|
||||
E('option', { 'value': 'round-robin', 'selected': config.strategy === 'round-robin' }, 'Round Robin - Equal distribution'),
|
||||
E('option', { 'value': 'weighted', 'selected': config.strategy === 'weighted' }, 'Weighted - Based on capacity'),
|
||||
E('option', { 'value': 'geo', 'selected': config.strategy === 'geo' }, 'Geographic - Nearest peer'),
|
||||
E('option', { 'value': 'latency', 'selected': config.strategy === 'latency' }, 'Latency - Fastest response')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Mesh Synchronization'),
|
||||
E('div', { 'class': 'deploy-options' }, [
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': true }),
|
||||
E('span', {}, '🔄 Real-time zone sync')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': true }),
|
||||
E('span', {}, '📊 Health-based routing')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': config.onionEnabled }),
|
||||
E('span', {}, '🧅 Onion relay fallback')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Upstream DNS (Failover)'),
|
||||
E('input', { 'type': 'text', 'class': 'form-input', 'value': '1.1.1.1, 8.8.8.8', 'placeholder': 'Comma-separated DNS servers' })
|
||||
])
|
||||
]),
|
||||
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', 'DNS Bridge configuration saved'), 'info');
|
||||
} }, 'Save')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
showWGMirrorModal: function() {
|
||||
var self = this;
|
||||
var config = this.wgMirrorConfig || {};
|
||||
|
||||
ui.showModal('WireGuard Mirror Configuration', [
|
||||
E('div', { 'class': 'modal-form' }, [
|
||||
E('div', { 'class': 'deploy-modal-header' }, [
|
||||
E('span', { 'class': 'deploy-modal-icon' }, '🪞'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'deploy-modal-title' }, 'WireGuard Mirror Inverse System'),
|
||||
E('div', { 'class': 'deploy-modal-subtitle' }, 'Bidirectional tunnel mirroring with automatic failover')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Mirror Mode'),
|
||||
E('select', { 'id': 'wg-mirror-mode', 'class': 'form-select' }, [
|
||||
E('option', { 'value': 'active-passive', 'selected': config.mode === 'active-passive' }, 'Active-Passive - Primary with standby'),
|
||||
E('option', { 'value': 'active-active', 'selected': config.mode === 'active-active' }, 'Active-Active - Load shared'),
|
||||
E('option', { 'value': 'ring', 'selected': config.mode === 'ring' }, 'Ring Topology - Circular routing'),
|
||||
E('option', { 'value': 'full-mesh', 'selected': config.mode === 'full-mesh' }, 'Full Mesh - All-to-all')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Sync Interval (seconds)'),
|
||||
E('input', { 'type': 'number', 'id': 'wg-sync-interval', 'class': 'form-input', 'value': config.syncInterval || 30 })
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Mirror Features'),
|
||||
E('div', { 'class': 'deploy-options' }, [
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': true }),
|
||||
E('span', {}, '🔑 Key rotation sync')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': true }),
|
||||
E('span', {}, '📋 Peer list mirroring')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': true }),
|
||||
E('span', {}, '🔄 Auto-reconnect on failure')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox' }),
|
||||
E('span', {}, '🔒 Inverse tunnel (bidirectional)')
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'modal-actions' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'),
|
||||
E('button', { 'class': 'cbi-button', 'click': function() { self.syncWGMirror(); ui.hideModal(); } }, '🔄 Sync Now'),
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', 'WireGuard Mirror configuration saved'), 'info');
|
||||
} }, 'Save')
|
||||
])
|
||||
]);
|
||||
},
|
||||
@ -883,6 +1250,93 @@ return view.extend({
|
||||
ui.addNotification(null, E('p', 'Copied: ' + text), 'info');
|
||||
},
|
||||
|
||||
// ==================== Deployment Actions ====================
|
||||
deployAllRegistry: function() {
|
||||
var self = this;
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
if (onlinePeers === 0) {
|
||||
ui.addNotification(null, E('p', 'No online peers to deploy to'), 'warning');
|
||||
return;
|
||||
}
|
||||
ui.addNotification(null, E('p', '📤 Deploying registry to ' + onlinePeers + ' peers...'), 'info');
|
||||
P2PAPI.deployRegistry().then(function(result) {
|
||||
ui.addNotification(null, E('p', '✅ Registry deployed to ' + (result.deployed_peers || onlinePeers) + ' peers'), 'success');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', '❌ Deploy failed: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
deployRegistryEntry: function(entry) {
|
||||
var self = this;
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
ui.addNotification(null, E('p', '📤 Deploying ' + entry.shortUrl + ' to mesh...'), 'info');
|
||||
P2PAPI.deployRegistryEntry(entry.shortUrl).then(function(result) {
|
||||
ui.addNotification(null, E('p', '✅ ' + entry.shortUrl + ' deployed to ' + (result.deployed_peers || onlinePeers) + ' peers'), 'success');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', '❌ Deploy failed: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
deployAllServices: function() {
|
||||
var self = this;
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
if (onlinePeers === 0) {
|
||||
ui.addNotification(null, E('p', 'No online peers to deploy to'), 'warning');
|
||||
return;
|
||||
}
|
||||
ui.addNotification(null, E('p', '⚡ Deploying all services to ' + onlinePeers + ' peers...'), 'info');
|
||||
P2PAPI.deployServices().then(function(result) {
|
||||
ui.addNotification(null, E('p', '✅ Services deployed: ' + (result.services_deployed || self.services.length) + ' to ' + (result.deployed_peers || onlinePeers) + ' peers'), 'success');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', '❌ Deploy failed: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
deployLocalServices: function() {
|
||||
var self = this;
|
||||
var localServices = this.getLocalServicesTyped();
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
ui.addNotification(null, E('p', '📤 Deploying ' + localServices.length + ' local services...'), 'info');
|
||||
P2PAPI.deployLocalServices().then(function(result) {
|
||||
ui.addNotification(null, E('p', '✅ Local services deployed to ' + (result.deployed_peers || onlinePeers) + ' peers'), 'success');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', '❌ Deploy failed: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
pullAllServices: function() {
|
||||
var self = this;
|
||||
ui.addNotification(null, E('p', '📥 Pulling services from mesh...'), 'info');
|
||||
P2PAPI.pullMeshServices().then(function(result) {
|
||||
ui.addNotification(null, E('p', '✅ Pulled ' + (result.services_pulled || 0) + ' services from mesh'), 'success');
|
||||
self.refreshData();
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', '❌ Pull failed: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
deployService: function(service) {
|
||||
var self = this;
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; }).length;
|
||||
ui.addNotification(null, E('p', '📤 Deploying ' + service.name + '...'), 'info');
|
||||
P2PAPI.deployService(service.id).then(function(result) {
|
||||
ui.addNotification(null, E('p', '✅ ' + service.name + ' deployed to ' + (result.deployed_peers || onlinePeers) + ' peers'), 'success');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', '❌ Deploy failed: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
pullService: function(service) {
|
||||
var self = this;
|
||||
ui.addNotification(null, E('p', '📥 Pulling ' + service.name + ' from ' + service.peer + '...'), 'info');
|
||||
P2PAPI.pullService(service.id, service.peer).then(function(result) {
|
||||
ui.addNotification(null, E('p', '✅ ' + service.name + ' pulled successfully'), 'success');
|
||||
self.refreshData();
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', '❌ Pull failed: ' + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== Modals ====================
|
||||
showRegisterURLModal: function() {
|
||||
var self = this;
|
||||
@ -1067,6 +1521,144 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
showDeployRegistryModal: function() {
|
||||
var self = this;
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; });
|
||||
var services = this.getRegisteredServices();
|
||||
|
||||
ui.showModal('Deploy Registry to Mesh', [
|
||||
E('div', { 'class': 'modal-form' }, [
|
||||
E('div', { 'class': 'deploy-modal-header' }, [
|
||||
E('span', { 'class': 'deploy-modal-icon' }, '🚀'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'deploy-modal-title' }, 'Master Deployment'),
|
||||
E('div', { 'class': 'deploy-modal-subtitle' }, 'Distribute registry entries across the mesh network')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Deployment Summary'),
|
||||
E('div', { 'class': 'deploy-summary' }, [
|
||||
E('div', { 'class': 'deploy-stat' }, [
|
||||
E('span', { 'class': 'ds-value' }, String(services.length)),
|
||||
E('span', { 'class': 'ds-label' }, 'Registry entries')
|
||||
]),
|
||||
E('div', { 'class': 'deploy-stat' }, [
|
||||
E('span', { 'class': 'ds-value' }, String(onlinePeers.length)),
|
||||
E('span', { 'class': 'ds-label' }, 'Target peers')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Target Peers'),
|
||||
E('div', { 'class': 'peer-checklist' },
|
||||
onlinePeers.map(function(peer) {
|
||||
return E('label', { 'class': 'peer-check-item' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': true, 'data-peer': peer.id }),
|
||||
E('span', { 'class': 'peer-check-icon' }, '🖥️'),
|
||||
E('span', {}, peer.name || peer.id),
|
||||
E('span', { 'class': 'peer-check-addr' }, peer.address || '')
|
||||
]);
|
||||
})
|
||||
)
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Deployment Options'),
|
||||
E('div', { 'class': 'deploy-options' }, [
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'id': 'deploy-sync-dns', 'checked': true }),
|
||||
E('span', {}, '🌐 Sync DNS records')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'id': 'deploy-update-lb', 'checked': true }),
|
||||
E('span', {}, '⚖️ Update load balancer')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'id': 'deploy-flush-cache' }),
|
||||
E('span', {}, '💾 Flush peer caches')
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
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();
|
||||
self.deployAllRegistry();
|
||||
} }, '🚀 Deploy Now')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
showDeployServicesModal: function() {
|
||||
var self = this;
|
||||
var onlinePeers = this.peers.filter(function(p) { return p.status === 'online'; });
|
||||
var localServices = this.getLocalServicesTyped();
|
||||
|
||||
ui.showModal('Service Distribution Configuration', [
|
||||
E('div', { 'class': 'modal-form' }, [
|
||||
E('div', { 'class': 'deploy-modal-header' }, [
|
||||
E('span', { 'class': 'deploy-modal-icon' }, '⚡'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'deploy-modal-title' }, 'Mesh Service Distribution'),
|
||||
E('div', { 'class': 'deploy-modal-subtitle' }, 'Configure how services are distributed across peers')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Distribution Strategy'),
|
||||
E('select', { 'id': 'deploy-strategy', 'class': 'form-select' }, [
|
||||
E('option', { 'value': 'replicate' }, '🔄 Replicate - All services to all peers'),
|
||||
E('option', { 'value': 'distribute' }, '📊 Distribute - Spread services across peers'),
|
||||
E('option', { 'value': 'failover' }, '🛡️ Failover - Primary with backup peers'),
|
||||
E('option', { 'value': 'custom' }, '⚙️ Custom - Manual assignment')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Services to Deploy (' + localServices.length + ' available)'),
|
||||
E('div', { 'class': 'service-checklist' },
|
||||
localServices.slice(0, 10).map(function(svc) {
|
||||
var type = self.serviceTypes[svc.type] || { icon: '❓', name: svc.type || 'unknown' };
|
||||
return E('label', { 'class': 'service-check-item' }, [
|
||||
E('input', { 'type': 'checkbox', 'checked': true, 'data-service': svc.id }),
|
||||
E('span', { 'class': 'svc-check-icon' }, type.icon),
|
||||
E('span', {}, svc.name),
|
||||
E('span', { 'class': 'svc-check-status ' + (svc.status === 'running' ? 'running' : 'stopped') },
|
||||
svc.status === 'running' ? '●' : '○')
|
||||
]);
|
||||
})
|
||||
)
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', {}, 'Load Balancing'),
|
||||
E('div', { 'class': 'deploy-options' }, [
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'id': 'deploy-enable-lb', 'checked': true }),
|
||||
E('span', {}, '⚖️ Enable HAProxy load balancing')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'id': 'deploy-health-checks', 'checked': true }),
|
||||
E('span', {}, '💓 Enable health checks')
|
||||
]),
|
||||
E('label', { 'class': 'deploy-option' }, [
|
||||
E('input', { 'type': 'checkbox', 'id': 'deploy-dns-round-robin' }),
|
||||
E('span', {}, '🌐 DNS round-robin')
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'modal-actions' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'),
|
||||
E('button', { 'class': 'cbi-button', 'click': function() {
|
||||
ui.hideModal();
|
||||
self.pullAllServices();
|
||||
} }, '📥 Pull from Mesh'),
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
|
||||
ui.hideModal();
|
||||
self.deployAllServices();
|
||||
} }, '🚀 Deploy to Mesh')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
// ==================== Styles ====================
|
||||
getStyles: function() {
|
||||
return [
|
||||
@ -1326,7 +1918,92 @@ return view.extend({
|
||||
'.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; }'
|
||||
'.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }',
|
||||
|
||||
// Deploy Banner
|
||||
'.deploy-banner { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: linear-gradient(135deg, rgba(102,126,234,0.15), rgba(118,75,162,0.15)); border: 1px solid rgba(102,126,234,0.3); border-radius: 10px; margin-bottom: 15px; }',
|
||||
'.deploy-banner.services { background: linear-gradient(135deg, rgba(16,185,129,0.15), rgba(52,152,219,0.15)); border-color: rgba(16,185,129,0.3); }',
|
||||
'.deploy-info { display: flex; align-items: center; gap: 12px; }',
|
||||
'.deploy-icon { font-size: 24px; }',
|
||||
'.deploy-title { font-weight: 600; font-size: 14px; }',
|
||||
'.deploy-desc { font-size: 12px; color: rgba(255,255,255,0.6); }',
|
||||
'.deploy-btn { padding: 8px 16px; background: linear-gradient(135deg, #667eea, #764ba2); border: none; border-radius: 8px; color: #fff; cursor: pointer; font-size: 12px; transition: all 0.3s; }',
|
||||
'.deploy-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }',
|
||||
'.deploy-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }',
|
||||
'.deploy-actions-mini { display: flex; gap: 8px; }',
|
||||
|
||||
// Mesh Status Dots
|
||||
'.mesh-status { display: flex; align-items: center; gap: 6px; }',
|
||||
'.mesh-dots { display: flex; gap: 3px; }',
|
||||
'.mesh-dots .dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.2); }',
|
||||
'.mesh-dots .dot.active { background: #10b981; }',
|
||||
'.mesh-status.full .mesh-dots .dot { background: #10b981; }',
|
||||
'.mesh-status.partial .mesh-dots .dot.active { background: #f59e0b; }',
|
||||
'.mesh-count { font-size: 10px; color: rgba(255,255,255,0.5); }',
|
||||
|
||||
// Deploy Entry Button
|
||||
'.deploy-entry-btn { width: 28px; height: 28px; border-radius: 6px; border: 1px solid rgba(102,126,234,0.3); background: rgba(102,126,234,0.1); cursor: pointer; font-size: 12px; transition: all 0.2s; }',
|
||||
'.deploy-entry-btn:hover { background: rgba(102,126,234,0.3); transform: scale(1.1); }',
|
||||
'.deploy-entry-btn.deployed { background: rgba(16,185,129,0.2); border-color: rgba(16,185,129,0.4); color: #10b981; }',
|
||||
|
||||
// Service Item with Deploy
|
||||
'.service-item.with-deploy { display: grid; grid-template-columns: auto 1fr auto auto; align-items: center; gap: 10px; }',
|
||||
'.svc-mesh-status { display: flex; align-items: center; gap: 4px; }',
|
||||
'.mesh-micro-dots { display: flex; gap: 2px; }',
|
||||
'.micro-dot { width: 4px; height: 4px; border-radius: 50%; background: rgba(255,255,255,0.2); }',
|
||||
'.micro-dot.active { background: #10b981; }',
|
||||
'.mesh-count-mini { font-size: 9px; color: rgba(255,255,255,0.4); }',
|
||||
'.svc-deploy-btn, .svc-pull-btn { width: 24px; height: 24px; border-radius: 4px; border: none; background: rgba(102,126,234,0.15); cursor: pointer; font-size: 11px; transition: all 0.2s; }',
|
||||
'.svc-deploy-btn:hover, .svc-pull-btn:hover { background: rgba(102,126,234,0.3); }',
|
||||
'.svc-deploy-btn.deployed { background: rgba(16,185,129,0.2); color: #10b981; }',
|
||||
'.svc-pull-btn { background: rgba(52,152,219,0.15); }',
|
||||
'.svc-pull-btn:hover { background: rgba(52,152,219,0.3); }',
|
||||
|
||||
// Column Title Deploy Buttons
|
||||
'.deploy-all-btn, .pull-all-btn { padding: 4px 8px; border: none; background: rgba(255,255,255,0.1); border-radius: 4px; cursor: pointer; font-size: 11px; margin-left: 8px; transition: all 0.2s; }',
|
||||
'.deploy-all-btn:hover { background: rgba(102,126,234,0.3); }',
|
||||
'.pull-all-btn:hover { background: rgba(52,152,219,0.3); }',
|
||||
|
||||
// Deploy Modal Styles
|
||||
'.deploy-modal-header { display: flex; align-items: center; gap: 15px; padding: 15px; background: rgba(0,0,0,0.2); border-radius: 10px; margin-bottom: 20px; }',
|
||||
'.deploy-modal-icon { font-size: 36px; }',
|
||||
'.deploy-modal-title { font-size: 18px; font-weight: 600; }',
|
||||
'.deploy-modal-subtitle { font-size: 12px; color: rgba(255,255,255,0.6); }',
|
||||
'.deploy-summary { display: flex; gap: 20px; padding: 15px; background: rgba(0,0,0,0.2); border-radius: 8px; }',
|
||||
'.deploy-stat { text-align: center; }',
|
||||
'.ds-value { font-size: 24px; font-weight: 700; color: #667eea; }',
|
||||
'.ds-label { font-size: 11px; color: rgba(255,255,255,0.5); }',
|
||||
'.peer-checklist, .service-checklist { max-height: 150px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 8px; }',
|
||||
'.peer-check-item, .service-check-item { display: flex; align-items: center; gap: 10px; padding: 8px; background: rgba(255,255,255,0.03); border-radius: 6px; cursor: pointer; }',
|
||||
'.peer-check-item:hover, .service-check-item:hover { background: rgba(255,255,255,0.06); }',
|
||||
'.peer-check-icon, .svc-check-icon { font-size: 16px; }',
|
||||
'.peer-check-addr { margin-left: auto; font-size: 11px; color: rgba(255,255,255,0.4); }',
|
||||
'.svc-check-status { margin-left: auto; font-size: 10px; }',
|
||||
'.svc-check-status.running { color: #10b981; }',
|
||||
'.svc-check-status.stopped { color: #ef4444; }',
|
||||
'.deploy-options { display: flex; flex-direction: column; gap: 8px; }',
|
||||
'.deploy-option { display: flex; align-items: center; gap: 10px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 6px; cursor: pointer; }',
|
||||
'.deploy-option:hover { background: rgba(0,0,0,0.3); }',
|
||||
|
||||
// Mesh Stack Advanced
|
||||
'.mesh-section-title { margin: 20px 0 15px 0; font-size: 13px; color: rgba(255,255,255,0.7); font-weight: 500; }',
|
||||
'.mesh-cards.advanced { grid-template-columns: repeat(3, 1fr); }',
|
||||
'@media (max-width: 1100px) { .mesh-cards.advanced { grid-template-columns: 1fr 1fr; } }',
|
||||
'@media (max-width: 700px) { .mesh-cards.advanced { grid-template-columns: 1fr; } }',
|
||||
'.mesh-card.featured { border: 1px solid rgba(102,126,234,0.3); background: linear-gradient(135deg, rgba(102,126,234,0.1), rgba(118,75,162,0.1)); }',
|
||||
'.mesh-card-actions { display: flex; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.08); }',
|
||||
'.mini-select { padding: 4px 8px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15); border-radius: 4px; color: #e0e0e0; font-size: 11px; }',
|
||||
'.status-indicator { font-size: 10px; }',
|
||||
'.status-indicator.active { color: #10b981; }',
|
||||
'.status-indicator.inactive { color: #95a5a6; }',
|
||||
'.onion-row { display: flex; align-items: center; gap: 8px; }',
|
||||
'.onion-icon { font-size: 14px; }',
|
||||
'.toggle-switch.mini { width: 32px; height: 18px; }',
|
||||
'.toggle-switch.mini .slider:before { height: 12px; width: 12px; left: 3px; bottom: 3px; }',
|
||||
'.toggle-switch.mini input:checked + .slider:before { transform: translateX(14px); }',
|
||||
|
||||
// Registry green stat value
|
||||
'.reg-stat-value.green { color: #2ecc71; }'
|
||||
].join('\n');
|
||||
},
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user