feat(p2p): Add self-peer testing and gigogne distribution mode
- Add self-peer for local mesh testing without remote nodes - Add gigogne (nested matryoshka) distribution mode with configurable depth - Add distribution mode selector: gigogne, mono, ring, full-mesh - Add visual indicators for self/gigogne peers on globe and peers panel - Add test mode badge and clear test button - Add rebuildGigogneStructure for dynamic mode switching - Update CSS with gigogne/self peer styles and animations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
afd761a239
commit
28dc602cda
@ -71,6 +71,18 @@ return view.extend({
|
|||||||
inverseTunnel: false
|
inverseTunnel: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Distribution Config - Gigogne (nested cycle)
|
||||||
|
distributionConfig: {
|
||||||
|
mode: 'gigogne', // gigogne = nested cycle, mono = single hop, full = all-to-all, ring = circular
|
||||||
|
cycleDepth: 3,
|
||||||
|
autoPropagate: true,
|
||||||
|
selfLoop: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Self-peer for testing
|
||||||
|
selfPeer: null,
|
||||||
|
testMode: false,
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@ -204,10 +216,15 @@ return view.extend({
|
|||||||
peerNodes.map(function(node) {
|
peerNodes.map(function(node) {
|
||||||
var opacity = 0.4 + (node.z + 1) * 0.3;
|
var opacity = 0.4 + (node.z + 1) * 0.3;
|
||||||
var scale = 0.6 + (node.z + 1) * 0.2;
|
var scale = 0.6 + (node.z + 1) * 0.2;
|
||||||
|
var peerClass = 'globe-peer';
|
||||||
|
if (node.peer.status === 'online') peerClass += ' online';
|
||||||
|
else peerClass += ' offline';
|
||||||
|
if (node.peer.isSelf) peerClass += ' self';
|
||||||
|
if (node.peer.isGigogne) peerClass += ' gigogne';
|
||||||
return E('div', {
|
return E('div', {
|
||||||
'class': 'globe-peer ' + (node.peer.status === 'online' ? 'online' : 'offline'),
|
'class': peerClass,
|
||||||
'style': 'left: ' + node.x + '%; top: ' + node.y + '%; opacity: ' + opacity + '; transform: scale(' + scale + ');',
|
'style': 'left: ' + node.x + '%; top: ' + node.y + '%; opacity: ' + opacity + '; transform: scale(' + scale + ');',
|
||||||
'title': (node.peer.name || node.peer.id) + ' - ' + (node.peer.address || 'Unknown')
|
'title': (node.peer.name || node.peer.id) + ' - ' + (node.peer.address || 'Unknown') + (node.peer.isGigogne ? ' [L' + node.peer.level + ']' : '')
|
||||||
}, [
|
}, [
|
||||||
E('span', { 'class': 'peer-dot' }),
|
E('span', { 'class': 'peer-dot' }),
|
||||||
E('span', { 'class': 'peer-label' }, node.peer.name || node.peer.id)
|
E('span', { 'class': 'peer-label' }, node.peer.name || node.peer.id)
|
||||||
@ -283,7 +300,31 @@ return view.extend({
|
|||||||
E('div', { 'class': 'globe-actions' }, [
|
E('div', { 'class': 'globe-actions' }, [
|
||||||
E('button', { 'class': 'globe-btn primary', 'click': function() { self.discoverPeers(); } }, '🔍 Discover'),
|
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.syncAll(); } }, '🔄 Sync All'),
|
||||||
E('button', { 'class': 'globe-btn', 'click': function() { self.showAddPeerModal(); } }, '➕ Add Peer')
|
E('button', { 'class': 'globe-btn', 'click': function() { self.showAddPeerModal(); } }, '➕ Add Peer'),
|
||||||
|
E('button', { 'class': 'globe-btn test', 'click': function() { self.addSelfPeer(); } }, '🔁 Self Peer')
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Distribution Mode Selector
|
||||||
|
E('div', { 'class': 'globe-distribution' }, [
|
||||||
|
E('span', { 'class': 'dist-label' }, '🪆 Distribution:'),
|
||||||
|
E('select', { 'class': 'dist-select', 'change': function(e) { self.setDistributionMode(e.target.value); } }, [
|
||||||
|
E('option', { 'value': 'gigogne', 'selected': this.distributionConfig.mode === 'gigogne' }, '🪆 Gigogne (Nested)'),
|
||||||
|
E('option', { 'value': 'mono', 'selected': this.distributionConfig.mode === 'mono' }, '1️⃣ Mono (Single)'),
|
||||||
|
E('option', { 'value': 'ring', 'selected': this.distributionConfig.mode === 'ring' }, '⭕ Ring (Cycle)'),
|
||||||
|
E('option', { 'value': 'full', 'selected': this.distributionConfig.mode === 'full' }, '🕸️ Full Mesh')
|
||||||
|
]),
|
||||||
|
E('span', { 'class': 'dist-depth' }, [
|
||||||
|
'Depth: ',
|
||||||
|
E('input', {
|
||||||
|
'type': 'number',
|
||||||
|
'class': 'depth-input',
|
||||||
|
'value': this.distributionConfig.cycleDepth,
|
||||||
|
'min': 1,
|
||||||
|
'max': 10,
|
||||||
|
'change': function(e) { self.distributionConfig.cycleDepth = parseInt(e.target.value); }
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
this.testMode ? E('span', { 'class': 'test-badge' }, '🧪 TEST') : null
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
@ -961,6 +1002,140 @@ return view.extend({
|
|||||||
ui.addNotification(null, E('p', 'WireGuard Mirror mode: ' + mode), 'info');
|
ui.addNotification(null, E('p', 'WireGuard Mirror mode: ' + mode), 'info');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ==================== Self Peer & Distribution ====================
|
||||||
|
addSelfPeer: function() {
|
||||||
|
var self = this;
|
||||||
|
this.testMode = true;
|
||||||
|
|
||||||
|
// Create self-peer with loopback
|
||||||
|
this.selfPeer = {
|
||||||
|
id: 'self-' + Date.now(),
|
||||||
|
name: '🔁 Self (Test)',
|
||||||
|
address: '127.0.0.1',
|
||||||
|
status: 'online',
|
||||||
|
isSelf: true,
|
||||||
|
services: this.services.slice(), // Mirror own services
|
||||||
|
wgMirror: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to peers list
|
||||||
|
this.peers.push(this.selfPeer);
|
||||||
|
|
||||||
|
// Create gigogne nested peers (matryoshka style)
|
||||||
|
var depth = this.distributionConfig.cycleDepth;
|
||||||
|
for (var i = 1; i < depth; i++) {
|
||||||
|
var nestedPeer = {
|
||||||
|
id: 'gigogne-' + i + '-' + Date.now(),
|
||||||
|
name: '🪆 Gigogne L' + i,
|
||||||
|
address: '127.0.0.' + (i + 1),
|
||||||
|
status: 'online',
|
||||||
|
isSelf: true,
|
||||||
|
isGigogne: true,
|
||||||
|
level: i,
|
||||||
|
parentId: i === 1 ? this.selfPeer.id : 'gigogne-' + (i - 1) + '-' + Date.now(),
|
||||||
|
services: this.services.slice(),
|
||||||
|
wgMirror: true
|
||||||
|
};
|
||||||
|
this.peers.push(nestedPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.addNotification(null, E('p', '🪆 Self peer added with ' + depth + ' gigogne levels (mono cycle)'), 'info');
|
||||||
|
|
||||||
|
// Trigger refresh
|
||||||
|
setTimeout(function() {
|
||||||
|
var container = document.querySelector('.p2p-hub-master');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.appendChild(self.render().firstChild);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSelfPeer: function() {
|
||||||
|
var self = this;
|
||||||
|
this.testMode = false;
|
||||||
|
this.selfPeer = null;
|
||||||
|
|
||||||
|
// Remove all test peers
|
||||||
|
this.peers = this.peers.filter(function(p) {
|
||||||
|
return !p.isSelf && !p.isGigogne;
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.addNotification(null, E('p', 'Self peer and gigogne levels removed'), 'info');
|
||||||
|
},
|
||||||
|
|
||||||
|
setDistributionMode: function(mode) {
|
||||||
|
this.distributionConfig.mode = mode;
|
||||||
|
var modeNames = {
|
||||||
|
'gigogne': '🪆 Gigogne (Nested Matryoshka)',
|
||||||
|
'mono': '1️⃣ Mono (Single Hop)',
|
||||||
|
'ring': '⭕ Ring (Circular Cycle)',
|
||||||
|
'full': '🕸️ Full Mesh (All-to-All)'
|
||||||
|
};
|
||||||
|
ui.addNotification(null, E('p', 'Distribution mode: ' + modeNames[mode]), 'info');
|
||||||
|
|
||||||
|
// If we have self-peer, recreate the gigogne structure
|
||||||
|
if (this.testMode && this.selfPeer) {
|
||||||
|
this.rebuildGigogneStructure();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rebuildGigogneStructure: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Remove existing gigogne peers
|
||||||
|
this.peers = this.peers.filter(function(p) {
|
||||||
|
return !p.isGigogne;
|
||||||
|
});
|
||||||
|
|
||||||
|
var mode = this.distributionConfig.mode;
|
||||||
|
var depth = this.distributionConfig.cycleDepth;
|
||||||
|
|
||||||
|
if (mode === 'gigogne') {
|
||||||
|
// Nested matryoshka - each level contains the next
|
||||||
|
for (var i = 1; i < depth; i++) {
|
||||||
|
this.peers.push({
|
||||||
|
id: 'gigogne-' + i,
|
||||||
|
name: '🪆 Gigogne L' + i,
|
||||||
|
address: '127.0.0.' + (i + 1),
|
||||||
|
status: 'online',
|
||||||
|
isGigogne: true,
|
||||||
|
level: i,
|
||||||
|
parentId: i === 1 ? this.selfPeer.id : 'gigogne-' + (i - 1),
|
||||||
|
services: this.services.slice()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (mode === 'ring') {
|
||||||
|
// Circular - each points to next, last points to first
|
||||||
|
for (var i = 1; i < depth; i++) {
|
||||||
|
this.peers.push({
|
||||||
|
id: 'ring-' + i,
|
||||||
|
name: '⭕ Ring N' + i,
|
||||||
|
address: '127.0.0.' + (i + 1),
|
||||||
|
status: 'online',
|
||||||
|
isGigogne: true,
|
||||||
|
level: i,
|
||||||
|
nextId: i < depth - 1 ? 'ring-' + (i + 1) : this.selfPeer.id,
|
||||||
|
services: this.services.slice()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (mode === 'mono') {
|
||||||
|
// Single hop - just one peer
|
||||||
|
this.peers.push({
|
||||||
|
id: 'mono-1',
|
||||||
|
name: '1️⃣ Mono Target',
|
||||||
|
address: '127.0.0.2',
|
||||||
|
status: 'online',
|
||||||
|
isGigogne: true,
|
||||||
|
level: 1,
|
||||||
|
services: this.services.slice()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// full mesh doesn't need special structure
|
||||||
|
|
||||||
|
ui.addNotification(null, E('p', 'Rebuilt ' + mode + ' structure with ' + (this.peers.length - 1) + ' nodes'), 'info');
|
||||||
|
},
|
||||||
|
|
||||||
syncWGMirror: function() {
|
syncWGMirror: function() {
|
||||||
ui.addNotification(null, E('p', '🔄 Syncing WireGuard mirror configurations...'), 'info');
|
ui.addNotification(null, E('p', '🔄 Syncing WireGuard mirror configurations...'), 'info');
|
||||||
},
|
},
|
||||||
@ -1086,27 +1261,45 @@ return view.extend({
|
|||||||
E('div', { 'class': 'panel-header orange' }, [
|
E('div', { 'class': 'panel-header orange' }, [
|
||||||
E('div', { 'class': 'panel-title' }, [
|
E('div', { 'class': 'panel-title' }, [
|
||||||
E('span', {}, '👥'),
|
E('span', {}, '👥'),
|
||||||
E('span', {}, 'Connected Peers')
|
E('span', {}, 'Connected Peers'),
|
||||||
|
this.testMode ? E('span', { 'class': 'badge test' }, '🧪 TEST') : null
|
||||||
]),
|
]),
|
||||||
E('button', { 'class': 'btn small', 'click': function() { self.discoverPeers(); } }, '🔍 Discover')
|
E('button', { 'class': 'btn small', 'click': function() { self.discoverPeers(); } }, '🔍 Discover')
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'peers-list' },
|
E('div', { 'class': 'peers-list' },
|
||||||
this.peers.length > 0 ?
|
this.peers.length > 0 ?
|
||||||
this.peers.map(function(p) {
|
this.peers.map(function(p) {
|
||||||
return E('div', { 'class': 'peer-row' }, [
|
var rowClass = 'peer-row';
|
||||||
E('span', { 'class': 'peer-icon' }, '🖥️'),
|
var icon = '🖥️';
|
||||||
|
if (p.isSelf) {
|
||||||
|
rowClass += ' self';
|
||||||
|
icon = '🔁';
|
||||||
|
} else if (p.isGigogne) {
|
||||||
|
rowClass += ' gigogne';
|
||||||
|
icon = '🪆';
|
||||||
|
}
|
||||||
|
return E('div', { 'class': rowClass }, [
|
||||||
|
E('span', { 'class': 'peer-icon' }, icon),
|
||||||
E('div', { 'class': 'peer-info' }, [
|
E('div', { 'class': 'peer-info' }, [
|
||||||
E('div', { 'class': 'peer-name' }, p.name || p.id),
|
E('div', { 'class': 'peer-name' }, p.name || p.id),
|
||||||
E('div', { 'class': 'peer-addr' }, p.address || 'Unknown')
|
E('div', { 'class': 'peer-addr' }, p.address || 'Unknown')
|
||||||
]),
|
]),
|
||||||
|
p.isGigogne ? E('span', { 'class': 'gigogne-level' }, 'L' + p.level) : null,
|
||||||
E('span', { 'class': 'peer-status ' + (p.status === 'online' ? 'online' : 'offline') }),
|
E('span', { 'class': 'peer-status ' + (p.status === 'online' ? 'online' : 'offline') }),
|
||||||
E('button', { 'class': 'btn-icon', 'click': function() { self.removePeer(p.id); } }, '✕')
|
(p.isSelf || p.isGigogne) ?
|
||||||
|
E('button', { 'class': 'btn-icon', 'click': function() { self.removeSelfPeer(); }, 'title': 'Remove test peers' }, '🗑️') :
|
||||||
|
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': 'empty-state' }, [
|
||||||
|
'No peers. ',
|
||||||
|
E('button', { 'class': 'btn small', 'click': function() { self.addSelfPeer(); } }, '🔁 Add Self for Testing')
|
||||||
|
])
|
||||||
),
|
),
|
||||||
E('div', { 'class': 'panel-actions' }, [
|
E('div', { 'class': 'panel-actions' }, [
|
||||||
E('button', { 'class': 'btn', 'click': function() { self.showAddPeerModal(); } }, '➕ Add Peer')
|
E('button', { 'class': 'btn', 'click': function() { self.showAddPeerModal(); } }, '➕ Add Peer'),
|
||||||
|
this.testMode ?
|
||||||
|
E('button', { 'class': 'btn', 'click': function() { self.removeSelfPeer(); } }, '🗑️ Clear Test') : null
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@ -2003,7 +2196,29 @@ return view.extend({
|
|||||||
'.toggle-switch.mini input:checked + .slider:before { transform: translateX(14px); }',
|
'.toggle-switch.mini input:checked + .slider:before { transform: translateX(14px); }',
|
||||||
|
|
||||||
// Registry green stat value
|
// Registry green stat value
|
||||||
'.reg-stat-value.green { color: #2ecc71; }'
|
'.reg-stat-value.green { color: #2ecc71; }',
|
||||||
|
|
||||||
|
// Self Peer & Distribution
|
||||||
|
'.globe-btn.test { background: linear-gradient(135deg, rgba(241,196,15,0.3), rgba(230,126,34,0.3)); border-color: rgba(241,196,15,0.5); }',
|
||||||
|
'.globe-btn.test:hover { background: linear-gradient(135deg, rgba(241,196,15,0.5), rgba(230,126,34,0.5)); }',
|
||||||
|
'.globe-distribution { display: flex; align-items: center; gap: 15px; margin-top: 15px; padding: 12px 15px; background: rgba(0,0,0,0.3); border-radius: 10px; flex-wrap: wrap; }',
|
||||||
|
'.dist-label { font-size: 13px; font-weight: 500; }',
|
||||||
|
'.dist-select { padding: 6px 12px; background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; color: #e0e0e0; font-size: 12px; }',
|
||||||
|
'.dist-depth { display: flex; align-items: center; gap: 8px; font-size: 12px; }',
|
||||||
|
'.depth-input { width: 50px; padding: 4px 8px; background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; color: #e0e0e0; font-size: 12px; text-align: center; }',
|
||||||
|
'.test-badge { padding: 4px 10px; background: linear-gradient(135deg, #f1c40f, #e67e22); color: #000; border-radius: 12px; font-size: 10px; font-weight: 700; animation: pulse-badge 2s ease-in-out infinite; }',
|
||||||
|
'@keyframes pulse-badge { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }',
|
||||||
|
|
||||||
|
// Gigogne peer styles
|
||||||
|
'.globe-peer.gigogne { }',
|
||||||
|
'.globe-peer.gigogne .peer-dot { background: linear-gradient(135deg, #f1c40f, #e67e22); box-shadow: 0 0 10px rgba(241,196,15,0.6); }',
|
||||||
|
'.globe-peer.self .peer-dot { background: linear-gradient(135deg, #667eea, #764ba2); box-shadow: 0 0 15px rgba(102,126,234,0.8); animation: self-pulse 1.5s ease-in-out infinite; }',
|
||||||
|
'@keyframes self-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.3); } }',
|
||||||
|
|
||||||
|
// Peer row gigogne indicator
|
||||||
|
'.peer-row.gigogne { border-left: 3px solid #f1c40f; margin-left: 10px; }',
|
||||||
|
'.peer-row.self { border-left: 3px solid #667eea; background: rgba(102,126,234,0.1); }',
|
||||||
|
'.gigogne-level { font-size: 10px; color: #f1c40f; margin-left: auto; padding: 2px 6px; background: rgba(241,196,15,0.2); border-radius: 4px; }'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user