From 4aeae8f5919a8e19273229567e75178041bdc835 Mon Sep 17 00:00:00 2001 From: Alex Godoroja Date: Mon, 23 Feb 2026 18:51:32 +0200 Subject: [PATCH 1/4] feat: display polo score in table --- pkg/registry/dashboard.go | 57 ++++- pkg/registry/server.go | 18 +- tests/pilot_dashboard/README.md | 44 ++++ tests/pilot_dashboard/run-dashboard.sh | 57 +++++ tests/pilot_dashboard/run_dashboard_test.go | 65 ++++++ tests/pilot_dashboard/seed_dashboard.go | 229 +++++++++++++++++++ tests/pilot_dashboard/seed_dashboard_main.go | 22 ++ 7 files changed, 473 insertions(+), 19 deletions(-) create mode 100644 tests/pilot_dashboard/README.md create mode 100755 tests/pilot_dashboard/run-dashboard.sh create mode 100644 tests/pilot_dashboard/run_dashboard_test.go create mode 100644 tests/pilot_dashboard/seed_dashboard.go create mode 100644 tests/pilot_dashboard/seed_dashboard_main.go diff --git a/pkg/registry/dashboard.go b/pkg/registry/dashboard.go index 7c74a10..c8f245a 100644 --- a/pkg/registry/dashboard.go +++ b/pkg/registry/dashboard.go @@ -190,9 +190,15 @@ tr:last-child td{border-bottom:none} .tag-filter{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px 12px;color:#c9d1d9;font-family:inherit;font-size:13px;width:100%;margin-bottom:12px;outline:none} .tag-filter:focus{border-color:#58a6ff} .tag-filter::placeholder{color:#484f58} -.task-badge{display:inline-block;background:#1a3a2a;border:1px solid #3fb950;border-radius:12px;padding:2px 10px;font-size:11px;color:#3fb950;white-space:nowrap} -.filter-row{display:flex;gap:12px;align-items:center;margin-bottom:12px} -.filter-row .tag-filter{margin-bottom:0;flex:1} +.sort-select{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px 12px;color:#c9d1d9;font-family:inherit;font-size:13px;cursor:pointer;outline:none} +.sort-select:focus{border-color:#58a6ff} +.task-badge{display:inline-block;background:#1a3a2a;border:1px solid:#3fb950;border-radius:12px;padding:2px 10px;font-size:11px;color:#3fb950;white-space:nowrap} +.polo-score{font-weight:600;color:#f59e0b} +.polo-high{color:#3fb950} +.polo-medium{color:#58a6ff} +.polo-low{color:#8b949e} +.filter-row{display:flex;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap} +.filter-row .tag-filter{margin-bottom:0;flex:1;min-width:200px} .filter-row label{font-size:13px;color:#8b949e;white-space:nowrap;cursor:pointer;display:flex;align-items:center;gap:4px} .empty{color:#484f58;font-style:italic;padding:20px;text-align:center} @@ -263,11 +269,19 @@ footer a:hover{color:#58a6ff}
+ +
- + - +
AddressStatusTrustTagsTasks
AddressStatusPOLO ScoreTrustTagsTasks
Loading...
Loading...
@@ -290,11 +304,27 @@ function uptimeStr(s){var d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Mat function getFiltered(){ var filter=document.getElementById('tag-filter').value; var taskOnly=document.getElementById('task-filter').checked; + var onlineOnly=document.getElementById('online-filter').checked; + var sortBy=document.getElementById('sort-select').value; var result=allNodes; if(filter){var q=filter.toLowerCase().replace(/^#/,'');result=result.filter(function(n){return n.tags&&n.tags.some(function(t){return t.indexOf(q)>=0})})} if(taskOnly){result=result.filter(function(n){return n.task_exec})} + if(onlineOnly){result=result.filter(function(n){return n.online})} + + // Apply sorting + if(sortBy==='polo_desc'){result.sort(function(a,b){return (b.polo_score||0)-(a.polo_score||0)})} + else if(sortBy==='polo_asc'){result.sort(function(a,b){return (a.polo_score||0)-(b.polo_score||0)})} + else if(sortBy==='trust_desc'){result.sort(function(a,b){return (b.trust_links||0)-(a.trust_links||0)})} + else if(sortBy==='online'){result.sort(function(a,b){return b.online-a.online})} + else{result.sort(function(a,b){return a.address.localeCompare(b.address)})} + return result; } +function getPoloClass(score){ + if(score>=100)return 'polo-high'; + if(score>=50)return 'polo-medium'; + return 'polo-low'; +} function renderNodes(){ var tb=document.getElementById('nodes-body'); tb.innerHTML=''; @@ -310,14 +340,17 @@ function renderNodes(){ var td2=document.createElement('td'); var dot=document.createElement('span');dot.style.cssText='display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;background:'+(n.online?'#3fb950':'#484f58'); td2.appendChild(dot);td2.appendChild(document.createTextNode(n.online?'Online':'Offline'));td2.style.color=n.online?'#3fb950':'#484f58'; - var td3=document.createElement('td');td3.textContent=n.trust_links||0;td3.style.color=n.trust_links?'#58a6ff':'#484f58'; - var td4=document.createElement('td'); - if(n.tags&&n.tags.length){n.tags.forEach(function(t){var s=document.createElement('span');s.className='tag';s.textContent='#'+t;td4.appendChild(s)})}else{td4.textContent='\u2014'} + var td3=document.createElement('td'); + var score=n.polo_score||0; + td3.textContent=score;td3.className='polo-score '+getPoloClass(score); + var td4=document.createElement('td');td4.textContent=n.trust_links||0;td4.style.color=n.trust_links?'#58a6ff':'#484f58'; var td5=document.createElement('td'); - if(n.task_exec){var b=document.createElement('span');b.className='task-badge';b.textContent='executor';td5.appendChild(b)}else{td5.textContent='\u2014'} - tr.appendChild(td1);tr.appendChild(td2);tr.appendChild(td3);tr.appendChild(td4);tr.appendChild(td5);tb.appendChild(tr); + if(n.tags&&n.tags.length){n.tags.forEach(function(t){var s=document.createElement('span');s.className='tag';s.textContent='#'+t;td5.appendChild(s)})}else{td5.textContent='\u2014'} + var td6=document.createElement('td'); + if(n.task_exec){var b=document.createElement('span');b.className='task-badge';b.textContent='executor';td6.appendChild(b)}else{td6.textContent='\u2014'} + tr.appendChild(td1);tr.appendChild(td2);tr.appendChild(td3);tr.appendChild(td4);tr.appendChild(td5);tr.appendChild(td6);tb.appendChild(tr); }); - }else{tb.innerHTML='No nodes'+(document.getElementById('tag-filter').value||document.getElementById('task-filter').checked?' matching filter':' registered')+''} + }else{tb.innerHTML='No nodes'+(document.getElementById('tag-filter').value||document.getElementById('task-filter').checked||document.getElementById('online-filter').checked?' matching filter':' registered')+''} var pg=document.getElementById('pagination'); if(filtered.length<=pageSize){pg.innerHTML='';return} pg.innerHTML=''; @@ -352,6 +385,8 @@ function update(){ } document.getElementById('tag-filter').addEventListener('input',function(){currentPage=1;renderNodes()}); document.getElementById('task-filter').addEventListener('change',function(){currentPage=1;renderNodes()}); +document.getElementById('online-filter').addEventListener('change',function(){currentPage=1;renderNodes()}); +document.getElementById('sort-select').addEventListener('change',function(){currentPage=1;renderNodes()}); update();setInterval(update,30000); diff --git a/pkg/registry/server.go b/pkg/registry/server.go index 06b7e3a..546234e 100644 --- a/pkg/registry/server.go +++ b/pkg/registry/server.go @@ -321,14 +321,14 @@ func NewWithStore(beaconAddr, storePath string) *Server { trustPairs: make(map[string]bool), handshakeInbox: make(map[uint32][]*HandshakeRelayMsg), handshakeResponses: make(map[uint32][]*HandshakeResponseMsg), - rateLimiter: NewRateLimiter(10, time.Minute), // 10 registrations per IP per minute - beacons: make(map[uint32]*beaconEntry), - replMgr: newReplicationManager(), - metrics: newRegistryMetrics(), - readyCh: make(chan struct{}), - done: make(chan struct{}), - saveCh: make(chan struct{}, 1), - saveDone: make(chan struct{}), + rateLimiter: NewRateLimiter(10, time.Minute), // 10 registrations per IP per minute + beacons: make(map[uint32]*beaconEntry), + replMgr: newReplicationManager(), + metrics: newRegistryMetrics(), + readyCh: make(chan struct{}), + done: make(chan struct{}), + saveCh: make(chan struct{}, 1), + saveDone: make(chan struct{}), } go s.saveLoop() @@ -2430,6 +2430,7 @@ type DashboardNode struct { Online bool `json:"online"` TrustLinks int `json:"trust_links"` TaskExec bool `json:"task_exec"` + PoloScore int `json:"polo_score"` } // DashboardNetwork is a public-safe view of a network for the dashboard. @@ -2521,6 +2522,7 @@ func (s *Server) GetDashboardStats() DashboardStats { Online: online, TrustLinks: trustCount[node.ID], TaskExec: node.TaskExec, + PoloScore: node.PoloScore, }) } diff --git a/tests/pilot_dashboard/README.md b/tests/pilot_dashboard/README.md new file mode 100644 index 0000000..5e68e60 --- /dev/null +++ b/tests/pilot_dashboard/README.md @@ -0,0 +1,44 @@ +# Dashboard Testing + +Test the Pilot Protocol dashboard with seeded data. + +## Quick Start + +```bash +./tests/pilot_dashboard/run-dashboard.sh +``` + +Dashboard available at: http://127.0.0.1:8080 + +Press Ctrl+C to stop. + +## Alternative (Go Test) + +```bash +go test -v -run TestRunDashboardWithSeed -timeout=0 ./tests/pilot_dashboard +``` + +## What You Get + +- 10 test nodes with hostnames (ml-gpu-1, webserver-1, etc.) +- Multiple tags (ml, gpu, webserver, database, etc.) +- 5 task executor nodes +- 10 trust relationships between nodes +- POLO scores ranging from 30 to 150 for reputation testing + +## Dashboard Features + +### Filtering +- **Tag filter**: Search nodes by tag +- **Tasks only**: Show only task executor nodes +- **Online only**: Show only online nodes + +### Sorting +- By Address (default) +- By POLO Score (High-Low or Low-High) +- By Trust Links (High-Low) +- By Status (Online first) + +### POLO Score Display +- Color-coded scores: Green (≥100), Blue (≥50), Gray (<50) +- Visible in dedicated column for easy comparison diff --git a/tests/pilot_dashboard/run-dashboard.sh b/tests/pilot_dashboard/run-dashboard.sh new file mode 100755 index 0000000..3485784 --- /dev/null +++ b/tests/pilot_dashboard/run-dashboard.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Script to run a local dashboard with seeded test data +# Usage: ./tests/pilot_dashboard/run-dashboard.sh + +set -e + +cd "$(dirname "$0")/../.." + +echo "Building binaries..." +make -s + +echo "" +echo "Starting rendezvous server with dashboard..." +echo "" + +# Start rendezvous in background +./bin/rendezvous \ + -registry-addr "127.0.0.1:9001" \ + -beacon-addr "127.0.0.1:9002" \ + -http "127.0.0.1:8080" & + +RENDEZVOUS_PID=$! + +# Cleanup on exit +cleanup() { + echo "" + echo "Shutting down rendezvous server..." + kill $RENDEZVOUS_PID 2>/dev/null || true + exit 0 +} +trap cleanup EXIT INT TERM + +# Wait for server to start +echo "Waiting for server to start..." +sleep 2 + +# Seed the registry +echo "" +echo "Seeding registry with test data..." +echo "" + +go run tests/pilot_dashboard/seed_dashboard_main.go 127.0.0.1:9001 + +echo "" +echo "======================================================================" +echo "✅ Dashboard is running!" +echo "" +echo " Dashboard URL: http://127.0.0.1:8080" +echo " Registry Addr: 127.0.0.1:9001" +echo " Beacon Addr: 127.0.0.1:9002" +echo "" +echo " Press Ctrl+C to stop the server" +echo "======================================================================" +echo "" + +# Wait for interrupt +wait $RENDEZVOUS_PID diff --git a/tests/pilot_dashboard/run_dashboard_test.go b/tests/pilot_dashboard/run_dashboard_test.go new file mode 100644 index 0000000..d4e78d6 --- /dev/null +++ b/tests/pilot_dashboard/run_dashboard_test.go @@ -0,0 +1,65 @@ +package pilot_dashboard + +import ( + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + "testing" + "time" + + "github.com/TeoSlayer/pilotprotocol/tests" +) + +// TestRunDashboardWithSeed is a manual test that starts a local rendezvous server, +// seeds it with test data, and keeps it running for manual dashboard inspection. +// +// Run with: go test -v -run TestRunDashboardWithSeed -timeout=0 ./tests/pilot_dashboard +// +// The dashboard will be available at: http://127.0.0.1:8080 +// Press Ctrl+C to stop the server. +func TestRunDashboardWithSeed(t *testing.T) { + if testing.Short() { + t.Skip("skipping manual dashboard test in short mode") + } + + log.Println("Starting rendezvous server with dashboard...") + + // Create test environment with dashboard enabled + env := tests.NewTestEnv(t) + + // Start dashboard on the registry + dashboardAddr := "127.0.0.1:8080" + go func() { + if err := env.Registry.ServeDashboard(dashboardAddr); err != nil { + log.Printf("dashboard error: %v", err) + } + }() + + // Wait a bit for dashboard to start + time.Sleep(500 * time.Millisecond) + + // Seed the registry with test data + log.Println("\nSeeding registry with test data...") + if err := SeedRegistry(env.RegistryAddr); err != nil { + t.Fatalf("failed to seed registry: %v", err) + } + + // Print access information + fmt.Println("\n" + strings.Repeat("=", 70)) + fmt.Printf("✅ Dashboard is running!\n\n") + fmt.Printf(" Dashboard URL: http://%s\n", dashboardAddr) + fmt.Printf(" Registry Addr: %s\n", env.RegistryAddr) + fmt.Printf(" Beacon Addr: %s\n\n", env.BeaconAddr) + fmt.Println(" Press Ctrl+C to stop the server") + fmt.Println(strings.Repeat("=", 70) + "\n") + + // Wait for interrupt signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + + log.Println("\nShutting down...") +} diff --git a/tests/pilot_dashboard/seed_dashboard.go b/tests/pilot_dashboard/seed_dashboard.go new file mode 100644 index 0000000..5f29280 --- /dev/null +++ b/tests/pilot_dashboard/seed_dashboard.go @@ -0,0 +1,229 @@ +package pilot_dashboard + +import ( + "encoding/base64" + "log" + "time" + + "github.com/TeoSlayer/pilotprotocol/internal/crypto" + "github.com/TeoSlayer/pilotprotocol/pkg/registry" +) + +// SeedRegistry populates a registry with test nodes, tags, and trust relationships +// for dashboard testing purposes. +func SeedRegistry(registryAddr string) error { + log.Printf("Connecting to registry at %s...", registryAddr) + + rc, err := registry.Dial(registryAddr) + if err != nil { + return err + } + defer rc.Close() + + // Seed test nodes with various configurations + nodes := []struct { + addr string + hostname string + tags []string + taskExec bool + poloScore int + }{ + {"192.168.1.10:8000", "ml-gpu-1", []string{"ml", "gpu", "training"}, true, 150}, + {"192.168.1.11:8000", "ml-gpu-2", []string{"ml", "gpu", "inference"}, true, 125}, + {"192.168.1.12:8000", "storage-1", []string{"storage", "backup"}, false, 45}, + {"192.168.1.13:8000", "compute-1", []string{"compute", "batch"}, true, 92}, + {"192.168.1.14:8000", "webserver-1", []string{"webserver", "api"}, true, 110}, + {"192.168.1.15:8000", "webserver-2", []string{"webserver", "frontend"}, false, 68}, + {"192.168.1.16:8000", "database-1", []string{"database", "postgres"}, false, 78}, + {"192.168.1.17:8000", "cache-1", []string{"cache", "redis"}, false, 55}, + {"192.168.1.18:8000", "assistant-1", []string{"assistant", "nlp"}, true, 135}, + {"192.168.1.19:8000", "monitor-1", []string{"monitoring", "metrics"}, false, 30}, + } + + registeredNodes := make([]struct { + id uint32 + identity *crypto.Identity + addr string + }, 0, len(nodes)) + + log.Println("Registering nodes...") + for i, n := range nodes { + // Generate identity for this node + id, err := crypto.GenerateIdentity() + if err != nil { + log.Printf("failed to generate identity for node %d: %v", i, err) + continue + } + + // Register node + msg := map[string]interface{}{ + "type": "register", + "listen_addr": n.addr, + "public_key": crypto.EncodePublicKey(id.PublicKey), + } + if n.hostname != "" { + msg["hostname"] = n.hostname + } + if n.taskExec { + msg["task_exec"] = true + } + + resp, err := rc.Send(msg) + if err != nil { + log.Printf("failed to register %s: %v", n.addr, err) + continue + } + + if resp["type"] != "register_ok" { + log.Printf("unexpected response for %s: %v", n.addr, resp) + continue + } + + nodeID := uint32(resp["node_id"].(float64)) + log.Printf("✓ Registered %s (ID: %d, hostname: %s)", n.addr, nodeID, n.hostname) + + // Store for later operations + registeredNodes = append(registeredNodes, struct { + id uint32 + identity *crypto.Identity + addr string + }{nodeID, id, n.addr}) + + // Set tags if any + if len(n.tags) > 0 { + // Create a new client with signer for authenticated operations + rcAuth, err := registry.Dial(registryAddr) + if err != nil { + log.Printf("failed to create auth client for %s: %v", n.addr, err) + continue + } + + // Set signer + rcAuth.SetSigner(func(challenge string) string { + sig := id.Sign([]byte(challenge)) + return base64.StdEncoding.EncodeToString(sig) + }) + + setTagsMsg := map[string]interface{}{ + "type": "set_tags", + "node_id": nodeID, + "tags": n.tags, + } + + tagResp, err := rcAuth.Send(setTagsMsg) + if err != nil { + log.Printf(" ⚠ failed to set tags for %s: %v", n.addr, err) + } else if tagResp["type"] == "set_tags_ok" { + log.Printf(" ✓ Set tags: %v", n.tags) + } + + rcAuth.Close() + } + + // Set POLO score if specified + if n.poloScore > 0 { + rcScore, err := registry.Dial(registryAddr) + if err != nil { + log.Printf(" ⚠ failed to create client for polo score: %v", err) + continue + } + + rcScore.SetSigner(func(challenge string) string { + sig := id.Sign([]byte(challenge)) + return base64.StdEncoding.EncodeToString(sig) + }) + + setScoreMsg := map[string]interface{}{ + "type": "set_polo_score", + "node_id": nodeID, + "polo_score": n.poloScore, + } + + scoreResp, err := rcScore.Send(setScoreMsg) + if err != nil { + log.Printf(" ⚠ failed to set POLO score for %s: %v", n.addr, err) + } else if scoreResp["type"] == "set_polo_score_ok" { + log.Printf(" ✓ Set POLO score: %d", n.poloScore) + } + + rcScore.Close() + } + + time.Sleep(100 * time.Millisecond) + } + + // Establish some trust relationships + log.Println("\nEstablishing trust relationships...") + trustPairs := [][2]int{ + {0, 1}, // ml-gpu-1 <-> ml-gpu-2 + {4, 5}, // webserver-1 <-> webserver-2 + {0, 8}, // ml-gpu-1 <-> assistant-1 + {2, 3}, // storage-1 <-> compute-1 + {6, 7}, // database-1 <-> cache-1 + } + + for _, pair := range trustPairs { + if pair[0] >= len(registeredNodes) || pair[1] >= len(registeredNodes) { + continue + } + + nodeA := registeredNodes[pair[0]] + nodeB := registeredNodes[pair[1]] + + // Create authenticated clients for both nodes + rcA, err := registry.Dial(registryAddr) + if err != nil { + continue + } + rcA.SetSigner(func(challenge string) string { + sig := nodeA.identity.Sign([]byte(challenge)) + return base64.StdEncoding.EncodeToString(sig) + }) + + rcB, err := registry.Dial(registryAddr) + if err != nil { + rcA.Close() + continue + } + rcB.SetSigner(func(challenge string) string { + sig := nodeB.identity.Sign([]byte(challenge)) + return base64.StdEncoding.EncodeToString(sig) + }) + + // Add trust A -> B + addTrustMsg := map[string]interface{}{ + "type": "add_trust", + "node_id": nodeA.id, + "peer_id": nodeB.id, + } + _, err = rcA.Send(addTrustMsg) + if err != nil { + log.Printf(" ⚠ failed to add trust %d -> %d: %v", nodeA.id, nodeB.id, err) + } else { + log.Printf(" ✓ Trust: %s -> %s", nodeA.addr, nodeB.addr) + } + + // Add trust B -> A + addTrustMsg = map[string]interface{}{ + "type": "add_trust", + "node_id": nodeB.id, + "peer_id": nodeA.id, + } + _, err = rcB.Send(addTrustMsg) + if err != nil { + log.Printf(" ⚠ failed to add trust %d -> %d: %v", nodeB.id, nodeA.id, err) + } else { + log.Printf(" ✓ Trust: %s -> %s", nodeB.addr, nodeA.addr) + } + + rcA.Close() + rcB.Close() + + time.Sleep(100 * time.Millisecond) + } + + log.Println("\n✅ Seeding complete!") + log.Printf("Dashboard should show %d nodes with tags and %d trust relationships", len(registeredNodes), len(trustPairs)*2) + + return nil +} diff --git a/tests/pilot_dashboard/seed_dashboard_main.go b/tests/pilot_dashboard/seed_dashboard_main.go new file mode 100644 index 0000000..a7206f5 --- /dev/null +++ b/tests/pilot_dashboard/seed_dashboard_main.go @@ -0,0 +1,22 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "log" + "os" + + "github.com/TeoSlayer/pilotprotocol/tests/pilot_dashboard" +) + +func main() { + if len(os.Args) < 2 { + log.Fatal("Usage: go run seed_dashboard_main.go \nExample: go run seed_dashboard_main.go 127.0.0.1:9001") + } + + registryAddr := os.Args[1] + if err := pilot_dashboard.SeedRegistry(registryAddr); err != nil { + log.Fatalf("failed to seed registry: %v", err) + } +} From c273aa8c880f0827449dc5d7ae8f069ce8ca1d58 Mon Sep 17 00:00:00 2001 From: Alex Godoroja Date: Mon, 23 Feb 2026 18:54:17 +0200 Subject: [PATCH 2/4] fix: run pre-commit tools --- .pre-commit-config.yaml | 2 +- cmd/pilotctl/main.go | 2 +- pkg/beacon/server_test.go | 4 ++-- pkg/daemon/handshake.go | 9 ++++----- pkg/daemon/ipc.go | 1 - pkg/daemon/services.go | 2 +- pkg/daemon/tunnel.go | 4 ++-- pkg/driver/ipc.go | 1 - tests/nat_traversal_test.go | 20 ++++++++++---------- 9 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f5cbd53..056d951 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,4 +28,4 @@ repos: language: system files: \.go$ pass_filenames: false - stages: [commit] + stages: [pre-commit] diff --git a/cmd/pilotctl/main.go b/cmd/pilotctl/main.go index d98d9fd..fbc9897 100644 --- a/cmd/pilotctl/main.go +++ b/cmd/pilotctl/main.go @@ -26,7 +26,7 @@ import ( "github.com/TeoSlayer/pilotprotocol/pkg/logging" "github.com/TeoSlayer/pilotprotocol/pkg/protocol" "github.com/TeoSlayer/pilotprotocol/pkg/registry" - "github.com/TeoSlayer/pilotprotocol/pkg/tasksubmit" + "github.com/TeoSlayer/pilotprotocol/pkg/tasksubmit" ) // Global flags diff --git a/pkg/beacon/server_test.go b/pkg/beacon/server_test.go index 30b9dbb..b99efb3 100644 --- a/pkg/beacon/server_test.go +++ b/pkg/beacon/server_test.go @@ -139,8 +139,8 @@ func TestCrossBeaconRelay(t *testing.T) { payload := []byte("hello from node 10") relayMsg := make([]byte, 1+4+4+len(payload)) relayMsg[0] = protocol.BeaconMsgRelay - binary.BigEndian.PutUint32(relayMsg[1:5], 10) // sender - binary.BigEndian.PutUint32(relayMsg[5:9], 20) // dest + binary.BigEndian.PutUint32(relayMsg[1:5], 10) // sender + binary.BigEndian.PutUint32(relayMsg[5:9], 20) // dest copy(relayMsg[9:], payload) if _, err := conn1.Write(relayMsg); err != nil { diff --git a/pkg/daemon/handshake.go b/pkg/daemon/handshake.go index cd54983..d856b7b 100644 --- a/pkg/daemon/handshake.go +++ b/pkg/daemon/handshake.go @@ -16,7 +16,6 @@ import ( "github.com/TeoSlayer/pilotprotocol/pkg/protocol" ) - // Handshake message types const ( HandshakeRequest = "handshake_request" @@ -55,11 +54,11 @@ type PendingHandshake struct { // Handshake timing constants const ( - handshakeMaxAge = 5 * time.Minute // replay protection: max message age - handshakeMaxFuture = 30 * time.Second // replay protection: max clock skew + handshakeMaxAge = 5 * time.Minute // replay protection: max message age + handshakeMaxFuture = 30 * time.Second // replay protection: max clock skew handshakeReapInterval = 5 * time.Minute // how often to reap stale replay entries - handshakeRecvTimeout = 10 * time.Second // time to wait for handshake message - handshakeCloseDelay = 500 * time.Millisecond // delay before closing after send to let data flush + handshakeRecvTimeout = 10 * time.Second // time to wait for handshake message + handshakeCloseDelay = 500 * time.Millisecond // delay before closing after send to let data flush ) // HandshakeManager handles the trust handshake protocol on port 444. diff --git a/pkg/daemon/ipc.go b/pkg/daemon/ipc.go index 717733d..21e2e51 100644 --- a/pkg/daemon/ipc.go +++ b/pkg/daemon/ipc.go @@ -779,4 +779,3 @@ func (s *IPCServer) DeliverDatagram(srcAddr protocol.Addr, srcPort uint16, dstPo } } } - diff --git a/pkg/daemon/services.go b/pkg/daemon/services.go index 657f93d..91a25d1 100644 --- a/pkg/daemon/services.go +++ b/pkg/daemon/services.go @@ -15,7 +15,7 @@ import ( "github.com/TeoSlayer/pilotprotocol/pkg/dataexchange" "github.com/TeoSlayer/pilotprotocol/pkg/eventstream" "github.com/TeoSlayer/pilotprotocol/pkg/protocol" - "github.com/TeoSlayer/pilotprotocol/pkg/registry" + "github.com/TeoSlayer/pilotprotocol/pkg/registry" "github.com/TeoSlayer/pilotprotocol/pkg/tasksubmit" ) diff --git a/pkg/daemon/tunnel.go b/pkg/daemon/tunnel.go index bc2f69c..874ba62 100644 --- a/pkg/daemon/tunnel.go +++ b/pkg/daemon/tunnel.go @@ -113,8 +113,8 @@ type TunnelManager struct { pending map[uint32][][]byte // node_id → queued frames // NAT traversal: beacon-coordinated hole-punching and relay - beaconAddr *net.UDPAddr // beacon address for punch/relay - relayPeers map[uint32]bool // peers that need relay (symmetric NAT) + beaconAddr *net.UDPAddr // beacon address for punch/relay + relayPeers map[uint32]bool // peers that need relay (symmetric NAT) // Webhook webhook *WebhookClient diff --git a/pkg/driver/ipc.go b/pkg/driver/ipc.go index a1bfcb6..9ac9165 100644 --- a/pkg/driver/ipc.go +++ b/pkg/driver/ipc.go @@ -293,4 +293,3 @@ func (c *ipcClient) unregisterRecvCh(connID uint32) { defer c.recvMu.Unlock() delete(c.recvChs, connID) } - diff --git a/tests/nat_traversal_test.go b/tests/nat_traversal_test.go index d859498..b2c974c 100644 --- a/tests/nat_traversal_test.go +++ b/tests/nat_traversal_test.go @@ -454,16 +454,16 @@ func TestNATScenarios(t *testing.T) { description string }{ { - name: "FullCone", - natType: "Full Cone (Endpoint Independent Mapping + Endpoint Independent Filtering)", + name: "FullCone", + natType: "Full Cone (Endpoint Independent Mapping + Endpoint Independent Filtering)", mechanism: "direct", description: "STUN-discovered endpoint works for all peers. " + "Any external host can send to the mapped address:port. " + "No hole-punching needed — direct tunnel works immediately.", }, { - name: "RestrictedCone", - natType: "Restricted Cone (Endpoint Independent Mapping + Address Restricted Filtering)", + name: "RestrictedCone", + natType: "Restricted Cone (Endpoint Independent Mapping + Address Restricted Filtering)", mechanism: "hole-punch", description: "Same external port for all destinations, but NAT only allows " + "return traffic from hosts we've sent to. Beacon coordinates simultaneous " + @@ -471,16 +471,16 @@ func TestNATScenarios(t *testing.T) { "the required NAT filter entries.", }, { - name: "PortRestrictedCone", - natType: "Port Restricted Cone (Endpoint Independent Mapping + Address+Port Restricted Filtering)", + name: "PortRestrictedCone", + natType: "Port Restricted Cone (Endpoint Independent Mapping + Address+Port Restricted Filtering)", mechanism: "hole-punch", description: "Like restricted cone but filtering checks both address AND port. " + "Still works with beacon hole-punching because both sides punch to the " + "exact STUN-discovered endpoint (which uses endpoint-independent mapping).", }, { - name: "Symmetric", - natType: "Symmetric (Endpoint Dependent Mapping)", + name: "Symmetric", + natType: "Symmetric (Endpoint Dependent Mapping)", mechanism: "relay", description: "Different external port for each destination. STUN port is only " + "valid for beacon, not for peers. Hole-punching fails because port is " + @@ -488,8 +488,8 @@ func TestNATScenarios(t *testing.T) { "and forwarded through the beacon server.", }, { - name: "CloudVM", - natType: "No NAT (Public IP / Cloud VM)", + name: "CloudVM", + natType: "No NAT (Public IP / Cloud VM)", mechanism: "direct", description: "Use -endpoint flag to specify the known public IP:port. " + "Skips STUN discovery entirely. Direct tunnel works immediately " + From 9fe730dc7aa52d3e4b91bc10d14443f8f2dbfd8b Mon Sep 17 00:00:00 2001 From: Alex Godoroja Date: Mon, 23 Feb 2026 18:58:16 +0200 Subject: [PATCH 3/4] fix: adjust colours --- pkg/registry/dashboard.go | 4 ++-- tests/pilot_dashboard/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/registry/dashboard.go b/pkg/registry/dashboard.go index c8f245a..3fce2b7 100644 --- a/pkg/registry/dashboard.go +++ b/pkg/registry/dashboard.go @@ -321,8 +321,8 @@ function getFiltered(){ return result; } function getPoloClass(score){ - if(score>=100)return 'polo-high'; - if(score>=50)return 'polo-medium'; + if(score>=50)return 'polo-high'; + if(score>=0)return 'polo-medium'; return 'polo-low'; } function renderNodes(){ diff --git a/tests/pilot_dashboard/README.md b/tests/pilot_dashboard/README.md index 5e68e60..21a3309 100644 --- a/tests/pilot_dashboard/README.md +++ b/tests/pilot_dashboard/README.md @@ -40,5 +40,5 @@ go test -v -run TestRunDashboardWithSeed -timeout=0 ./tests/pilot_dashboard - By Status (Online first) ### POLO Score Display -- Color-coded scores: Green (≥100), Blue (≥50), Gray (<50) +- Color-coded scores: Green (≥50), Blue (≥0), Gray (<0) - Visible in dedicated column for easy comparison From b780f95d8bc1eaace56482f145ca98faff301825 Mon Sep 17 00:00:00 2001 From: Alex Godoroja Date: Wed, 25 Feb 2026 16:39:40 +0200 Subject: [PATCH 4/4] feat: display total nodes in dashboard --- pkg/registry/dashboard.go | 15 ++++++++++++--- pkg/registry/server.go | 22 ++++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/pkg/registry/dashboard.go b/pkg/registry/dashboard.go index 3fce2b7..d4e4e2f 100644 --- a/pkg/registry/dashboard.go +++ b/pkg/registry/dashboard.go @@ -173,7 +173,7 @@ header h1{font-size:20px;font-weight:600;color:#e6edf3} header .links{display:flex;gap:16px;font-size:13px} .uptime{font-size:12px;color:#8b949e;margin-top:4px} -.stats-row{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:32px} +.stats-row{display:grid;grid-template-columns:repeat(6,1fr);gap:16px;margin-bottom:32px} .stat-card{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:20px;text-align:center} .stat-card .value{font-size:32px;font-weight:700;color:#e6edf3;display:block} .stat-card .label{font-size:12px;color:#8b949e;text-transform:uppercase;letter-spacing:0.5px;margin-top:4px} @@ -236,6 +236,10 @@ footer a:hover{color:#58a6ff} Total Requests +
+ + Total Nodes +
Online Nodes @@ -257,7 +261,7 @@ footer a:hover{color:#58a6ff}

Networks

- + @@ -362,6 +366,7 @@ function renderNodes(){ function update(){ fetch('/api/stats').then(function(r){return r.json()}).then(function(d){ document.getElementById('total-requests').textContent=fmt(d.total_requests); + document.getElementById('total-nodes').textContent=fmt(d.total_nodes||0); document.getElementById('active-nodes').textContent=fmt(d.active_nodes||0); document.getElementById('trust-links').textContent=fmt(d.total_trust_links||0); document.getElementById('unique-tags').textContent=fmt(d.unique_tags||0); @@ -374,7 +379,11 @@ function update(){ var tr=document.createElement('tr'); var td1=document.createElement('td');td1.textContent=n.id; var td2=document.createElement('td');td2.textContent=n.name; - var td3=document.createElement('td');td3.textContent=n.members; + var td3=document.createElement('td'); + var onlineMembers=n.online_members||0; + var totalMembers=n.members||0; + td3.textContent=onlineMembers+' / '+totalMembers; + if(onlineMembers>0){td3.style.color='#3fb950'}else{td3.style.color='#8b949e'} tr.appendChild(td1);tr.appendChild(td2);tr.appendChild(td3);nb.appendChild(tr); }); }else{nb.innerHTML=''} diff --git a/pkg/registry/server.go b/pkg/registry/server.go index 546234e..dbd0349 100644 --- a/pkg/registry/server.go +++ b/pkg/registry/server.go @@ -2435,9 +2435,10 @@ type DashboardNode struct { // DashboardNetwork is a public-safe view of a network for the dashboard. type DashboardNetwork struct { - ID uint16 `json:"id"` - Name string `json:"name"` - Members int `json:"members"` + ID uint16 `json:"id"` + Name string `json:"name"` + Members int `json:"members"` + OnlineMembers int `json:"online_members"` } // DashboardEdge represents a trust relationship between two nodes. @@ -2533,10 +2534,19 @@ func (s *Server) GetDashboardStats() DashboardStats { networks := make([]DashboardNetwork, 0, len(s.networks)) for _, net := range s.networks { + onlineCount := 0 + for _, memberID := range net.Members { + if node, exists := s.nodes[memberID]; exists { + if node.LastSeen.After(onlineThreshold) { + onlineCount++ + } + } + } networks = append(networks, DashboardNetwork{ - ID: net.ID, - Name: net.Name, - Members: len(net.Members), + ID: net.ID, + Name: net.Name, + Members: len(net.Members), + OnlineMembers: onlineCount, }) }
IDNameMembers
IDNameMembers (Online/Total)
Loading...
No networks