feat(mesh): show federated Archipelago nodes on the Mesh Map
Peers that opt in via a new "Share Location" toggle in Settings (server.set-location RPC) get plotted on other trusted peers' Mesh Map with a distinct Archy-logo marker, separate from raw LoRa radio peers. Location is persisted locally, carried in NodeStateSnapshot, and propagated through federation sync/delta like other node state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e3baaa5de3
commit
177b8a4338
@@ -13,7 +13,7 @@ const markersLayer = ref<L.LayerGroup | null>(null)
|
||||
const linesLayer = ref<L.LayerGroup | null>(null)
|
||||
|
||||
// Whether we have any position data to show
|
||||
const hasPositions = computed(() => mesh.nodePositions.size > 0)
|
||||
const hasPositions = computed(() => mesh.nodePositions.size > 0 || mesh.federatedPositions.size > 0)
|
||||
|
||||
// Location sharing state
|
||||
const sharingLocation = ref(false)
|
||||
@@ -108,6 +108,59 @@ function createMarkerIcon(type: 'self' | 'online' | 'offline'): L.DivIcon {
|
||||
})
|
||||
}
|
||||
|
||||
/// Marker for an Archipelago node (federation peer) — the little Archy logo
|
||||
/// in a glowing badge, distinct from the plain colored dots used for raw
|
||||
/// LoRa mesh-radio peers. Trusted nodes get the warm orange ring/glow the
|
||||
/// rest of the map already uses for "this is us/ours"; Observer nodes get a
|
||||
/// cooler blue so the trust boundary is visible at a glance.
|
||||
function createFederatedMarkerIcon(trusted: boolean): L.DivIcon {
|
||||
const ring = trusted ? '#fb923c' : '#38bdf8'
|
||||
const glow = trusted ? 'rgba(251,146,60,0.55)' : 'rgba(56,189,248,0.5)'
|
||||
const size = 30
|
||||
return L.divIcon({
|
||||
className: 'mesh-map-marker-wrapper',
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2],
|
||||
popupAnchor: [0, -(size / 2 + 2)],
|
||||
html: `
|
||||
<div style="
|
||||
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
|
||||
width:${size + 14}px; height:${size + 14}px; border-radius:50%;
|
||||
background:${glow}; animation:mesh-map-pulse 2.4s infinite;
|
||||
"></div>
|
||||
<div style="
|
||||
width:${size}px; height:${size}px; border-radius:9px;
|
||||
background:#0b0d14; border:2px solid ${ring};
|
||||
box-shadow:0 0 10px ${glow}, 0 2px 6px rgba(0,0,0,0.5);
|
||||
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
z-index:3; overflow:hidden;
|
||||
">
|
||||
<img src="/assets/icon/apple-touch-icon-180x180-v2.png" width="${size - 6}" height="${size - 6}"
|
||||
style="border-radius:6px;object-fit:cover;" alt="" />
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
function buildFederatedPopupContent(name: string, onion: string, trusted: boolean): string {
|
||||
const badge = trusted
|
||||
? '<span style="display:inline-block;background:rgba(251,146,60,0.2);color:#fb923c;font-size:0.65rem;padding:1px 6px;border-radius:4px;margin-left:6px;font-weight:600;">TRUSTED</span>'
|
||||
: '<span style="display:inline-block;background:rgba(56,189,248,0.2);color:#38bdf8;font-size:0.65rem;padding:1px 6px;border-radius:4px;margin-left:6px;font-weight:600;">OBSERVER</span>'
|
||||
const onionShort = onion.length > 20 ? `${onion.slice(0, 10)}...${onion.slice(-6)}` : onion
|
||||
return `
|
||||
<div style="font-family:'Avenir Next',sans-serif;min-width:170px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;font-weight:600;font-size:0.9rem;color:#fff;margin-bottom:4px;">
|
||||
📡 ${name}${badge}
|
||||
</div>
|
||||
<div style="font-size:0.72rem;color:rgba(255,255,255,0.5);font-family:monospace;margin-bottom:6px;word-break:break-all;">
|
||||
${onionShort}
|
||||
</div>
|
||||
<div style="font-size:0.78rem;color:rgba(255,255,255,0.6);">Archipelago node</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function getSignalBars(rssi: number | null): string {
|
||||
if (rssi === null) return 'Unknown'
|
||||
if (rssi >= -70) return 'Strong'
|
||||
@@ -218,7 +271,8 @@ function updateMarkers() {
|
||||
linesLayer.value.clearLayers()
|
||||
|
||||
const positions = mesh.nodePositions
|
||||
if (positions.size === 0) return
|
||||
const fedPositions = mesh.federatedPositions
|
||||
if (positions.size === 0 && fedPositions.size === 0) return
|
||||
|
||||
const bounds: L.LatLngExpression[] = []
|
||||
const selfPos = positions.get(-1)
|
||||
@@ -267,6 +321,22 @@ function updateMarkers() {
|
||||
}
|
||||
})
|
||||
|
||||
// Archipelago nodes (federation peers who opted into location sharing) —
|
||||
// own logo marker, no signal/hops (that's a radio-peer concept).
|
||||
fedPositions.forEach((fed) => {
|
||||
const marker = L.marker([fed.lat, fed.lng], {
|
||||
icon: createFederatedMarkerIcon(fed.trusted),
|
||||
zIndexOffset: 500,
|
||||
})
|
||||
marker.bindPopup(buildFederatedPopupContent(fed.name ?? 'Archipelago Node', fed.onion, fed.trusted), {
|
||||
className: 'mesh-map-popup',
|
||||
closeButton: true,
|
||||
maxWidth: 250,
|
||||
})
|
||||
markersLayer.value!.addLayer(marker)
|
||||
bounds.push([fed.lat, fed.lng])
|
||||
})
|
||||
|
||||
// Fit map to show all markers
|
||||
if (bounds.length > 1) {
|
||||
map.fitBounds(L.latLngBounds(bounds), { padding: [40, 40], maxZoom: 14 })
|
||||
@@ -283,7 +353,7 @@ function handleResize() {
|
||||
|
||||
// Watch for changes in node positions and peers
|
||||
watch(
|
||||
() => [mesh.nodePositions.size, mesh.peers.length],
|
||||
() => [mesh.nodePositions.size, mesh.peers.length, mesh.federatedPositions.size],
|
||||
() => {
|
||||
updateMarkers()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user