Skip to content

feat: add polo score and display total nodes in dashboard#7

Merged
TeoSlayer merged 4 commits into
mainfrom
show_polo_score
Feb 27, 2026
Merged

feat: add polo score and display total nodes in dashboard#7
TeoSlayer merged 4 commits into
mainfrom
show_polo_score

Conversation

@Alexgodoroja

@Alexgodoroja Alexgodoroja commented Feb 23, 2026

Copy link
Copy Markdown
Collaborator

Add POLO Score and Display Total Nodes in Dashboard

Screenshot 2026-02-25 at 16 34 43 Screenshot 2026-02-23 at 18 50 18

Overview

This PR:

  1. enhances the registry dashboard to display POLO scores for each node, providing visibility into node reputation and performance. It includes comprehensive filtering and sorting capabilities to help users identify high-performing nodes at a glance.
  2. Shows total registered Nodes alongside online nodes

Changes

Backend (Registry Server)

  • Added PoloScore field to DashboardNode struct (pkg/registry/server.go)
    • Exposes POLO score in the dashboard API response
    • Integrates with existing node reputation system

Frontend (Dashboard UI)

  • New POLO Score Column

    • Displays each node's POLO score between Status and Trust columns
    • Color-coded visualization:
      • 🟢 Green (≥50): High reputation nodes
      • 🔵 Blue (≥0): Medium reputation nodes
      • ⚪ Gray (<0): Low reputation nodes
  • Enhanced Filtering

    • Filter by tag (existing)
    • Task executors only (existing)
    • NEW: Online nodes only - Filter to show only active nodes
  • Advanced Sorting Options

    • Sort by Address (default)
    • NEW: Sort by POLO Score (High-Low) - Find top performers
    • NEW: Sort by POLO Score (Low-High) - Identify nodes needing attention
    • NEW: Sort by Trust Links (High-Low) - Most trusted nodes first
    • NEW: Sort by Status (Online first) - Prioritize active nodes

Testing Infrastructure

  • Created tests/pilot_dashboard/ directory

    • seed_dashboard.go - Core seeding logic with POLO scores
    • run_dashboard_test.go - Go test for manual dashboard testing
    • run-dashboard.sh - Bash script for quick local deployment
    • seed_dashboard_main.go - Standalone seed utility
    • README.md - Usage documentation
  • Test Data includes:

    • 10 nodes with realistic hostnames
    • POLO scores ranging from 30 to 150
    • Multiple tags (ml, gpu, webserver, database, etc.)
    • 5 task executor nodes
    • 10 bidirectional trust relationships

Usage

View Dashboard with Test Data

./tests/pilot_dashboard/run-dashboard.sh

Then visit: http://127.0.0.1:8080

Run via Go Test

go test -v -run TestRunDashboardWithSeed -timeout=0 ./tests/pilot_dashboard

Screenshots

The dashboard now shows:

  • POLO Score column with color-coded values
  • Dropdown menu for sorting by various criteria
  • Additional "Online only" filter checkbox
  • All existing functionality preserved

Benefits

  1. Transparency - POLO scores visible for all nodes
  2. Easy Discovery - Sort to find highest/lowest scoring nodes
  3. Better Monitoring - Quickly identify underperforming nodes
  4. Flexible Filtering - Combine multiple filters for precise queries
  5. Developer-Friendly - Simple test infrastructure for dashboard development

Breaking Changes

None. This is a backward-compatible enhancement.

Testing

  • ✅ All existing tests pass
  • ✅ Pre-commit hooks configured and running
  • ✅ Dashboard compiles and renders correctly
  • ✅ Seed script successfully populates test data
  • ✅ Filtering and sorting work as expected

Related Issues

Addresses the need for POLO score visibility in the dashboard UI.

@Alexgodoroja Alexgodoroja self-assigned this Feb 23, 2026
@Alexgodoroja Alexgodoroja added the enhancement New feature or request label Feb 23, 2026
@Alexgodoroja Alexgodoroja changed the title feat: Show polo score within polo dashboard feat: add polo score and display total nodes in dashboard Feb 25, 2026

@TeoSlayer TeoSlayer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks ok to me.

@TeoSlayer TeoSlayer merged commit a049bc4 into main Feb 27, 2026
2 checks passed
@TeoSlayer TeoSlayer deleted the show_polo_score branch February 27, 2026 07:52
TeoSlayer pushed a commit that referenced this pull request Apr 30, 2026
* feat: display polo score in table

* fix: run pre-commit tools

* fix: adjust colours

* feat: display total nodes in dashboard

---------

Co-authored-by: Alex Godoroja <alex@vulturelabs.io>
TeoSlayer pushed a commit that referenced this pull request May 3, 2026
Symptom: a peer pair establishes a tunnel, exchanges some traffic,
then both sides go idle. Consumer-grade NATs purge UDP mappings
after 30-90 s of no outbound traffic; CGNAT and enterprise NATs
after a few minutes. When either side eventually wants to send,
their packet arrives at the peer's NAT but there's no inbound
mapping → silently dropped. Recovery requires the peer's app to
send first AND for that send to trigger a key-exchange (only true
if our pc became un-decryptable, which usually it hasn't).

This is the CAUSE; iter 5 (NAT-remap address-learning) fixes the
CONSEQUENCE. Together they ensure long-idle peer pairs survive
NAT mapping churn.

Why per-Connection keepalives don't cover this: idleSweepLoop only
sends keepalive ACKs for connections in StateEstablished. Tunnel
peer state (tm.peers + tm.crypto) outlives individual connections.
A long-lived peer relationship with bursty connection cycles can
have NO active Connection for hours, during which no keepalive
fires.

Lands in this RED commit (compile-clean half of the fix):
  - lastOutboundSend map[uint32]time.Time field on TunnelManager,
    init in NewTunnelManager, cleared in RemovePeer
  - recordOutboundSend(nodeID) helper called from writeFrame's two
    success paths (relay-wrapped + direct). Updates timestamp
    under tm.mu.
  - TunnelKeepaliveInterval = 25s tunable var (below consumer-NAT
    lower bound of ~30s)
  - keepaliveSweep(now) STUB that returns 0 — body lands in GREEN

Tests:
  - TestKeepaliveAbsentForIdlePeer: pins stub returns 0 and peer's
    socket sees zero unsolicited frames in 150ms — current bug.
    GREEN flips: sweep returns 1 + peer socket receives a frame.
  - TestRecordOutboundSendStampsTimestamp: pins the new tracking —
    writeFrame stamps tm.lastOutboundSend on success.
TeoSlayer pushed a commit that referenced this pull request May 3, 2026
Bug fixed: tunnel-layer NAT keepalive. A goroutine spawned from
TunnelManager.Listen() now wakes every 5 s, identifies peers in
tm.peers whose lastOutboundSend is older than TunnelKeepaliveInterval
(25 s default — below consumer-NAT 30-90 s lower bound), and emits
one tiny encrypted ping per stale peer. Peers' NAT mappings stay
warm; a long-idle peer pair survives indefinitely without their
mappings expiring and silently breaking the next outbound send.

Why per-Connection keepalives don't cover this: idleSweepLoop only
sends ACK probes for ESTABLISHED Connection objects. Tunnel peer
state outlives Connections — a long-lived relationship with bursty
connection cycles can have no active Connection for hours.

Implementation (pkg/daemon/tunnel.go):
  - keepaliveSweep(now): scans tm.peers under RLock, builds a slice
    of stale peers (lastOutboundSend missing or older than
    TunnelKeepaliveInterval, pc != nil && pc.ready), then encrypts
    + writeFrame's an empty ProtoControl/PortPing packet to each.
    Returns count for tests.
  - keepaliveLoop: ticker at keepaliveTickerInterval (5s default),
    drives keepaliveSweep until tm.done closes.
  - Listen(): spawns keepaliveLoop alongside readLoop and
    rekeyRetransmitLoop.
  - handleEncrypted: drops isTunnelKeepalive(pkt) packets BEFORE
    recvCh send but AFTER all liveness side-effects (recordInbound
    Decrypt, clearRelayOnDirect, address-learning). Application
    layer never sees keepalives; the side-effects keep flowing.

Authentication invariant: keepalives go through encryptFrame, which
seals AAD = local nodeID. Peer's AEAD verifies that AAD on receipt.
An attacker can't forge keepalives without our private key, so they
can't keep stale entries warm or drive relay decisions.

Tests:
  - TestKeepaliveAbsentForIdlePeer: 1-minute-idle peer → sweep
    returns 1, peer socket receives 1 PILS-magic-prefixed encrypted
    frame, lastOutboundSend refreshed.
  - TestKeepaliveSweepSkipsRecentlyActivePeers: 1s-idle peer → sweep
    returns 0 (no redundant traffic).
  - TestHandleEncryptedDropsKeepaliveBeforeRecvCh: inbound keepalive
    sets lastDirectRecv but does NOT appear on recvCh.
  - TestRecordOutboundSendStampsTimestamp: writeFrame stamps
    lastOutboundSend on success.

Race-clean across full pkg/daemon (62s).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants