From 2d91a8f7ebf6b53176288e70571173465ff12618 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 30 Jan 2026 11:55:21 +0100 Subject: [PATCH] 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 --- .../resources/view/secubox-p2p/hub.js | 235 +++++++++++++++++- 1 file changed, 225 insertions(+), 10 deletions(-) diff --git a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js index d982301a..605d3762 100644 --- a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js +++ b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/hub.js @@ -71,6 +71,18 @@ return view.extend({ 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() { var self = this; return Promise.all([ @@ -204,10 +216,15 @@ return view.extend({ peerNodes.map(function(node) { var opacity = 0.4 + (node.z + 1) * 0.3; 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', { - 'class': 'globe-peer ' + (node.peer.status === 'online' ? 'online' : 'offline'), + 'class': peerClass, '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-label' }, node.peer.name || node.peer.id) @@ -283,7 +300,31 @@ return view.extend({ 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') + 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'); }, + // ==================== 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() { 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-title' }, [ 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('div', { 'class': 'peers-list' }, this.peers.length > 0 ? this.peers.map(function(p) { - return E('div', { 'class': 'peer-row' }, [ - E('span', { 'class': 'peer-icon' }, 'πŸ–₯️'), + var rowClass = 'peer-row'; + 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-name' }, p.name || p.id), 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('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('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); }', // 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'); },