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:
archipelago
2026-07-01 12:04:31 -04:00
co-authored by Claude Sonnet 5
parent e3baaa5de3
commit 177b8a4338
15 changed files with 337 additions and 4 deletions
+46
View File
@@ -179,6 +179,19 @@ export interface NodePosition {
timestamp: string
}
/// An Archipelago node (federation peer), not a raw LoRa radio peer — shown
/// on the Mesh Map with its own marker (the Archy logo) since it's a whole
/// server, not a mesh-radio contact. Only ones that opted into
/// server.set-location's `share` flag ever carry a lat/lon.
export interface FederatedNodePosition {
did: string
lat: number
lng: number
name: string | null
onion: string
trusted: boolean
}
export const useMeshStore = defineStore('mesh', () => {
const status = ref<MeshStatus | null>(null)
const peers = ref<MeshPeer[]>([])
@@ -192,6 +205,10 @@ export const useMeshStore = defineStore('mesh', () => {
// Node position tracking for map view (contact_id -> position)
const nodePositions = ref<Map<number, NodePosition>>(new Map())
// Federated Archipelago nodes with a shared location (DID -> position) —
// separate from nodePositions since these are whole servers, not radio
// peers, and render with a different marker on the Mesh Map.
const federatedPositions = ref<Map<string, FederatedNodePosition>>(new Map())
// Track unread message counts per peer (contact_id -> count)
const unreadCounts = ref<Record<number, number>>({})
@@ -340,6 +357,33 @@ export const useMeshStore = defineStore('mesh', () => {
})
}
// Federation nodes that opted into sharing their location (server.set-location
// `share: true`) — fed by Mesh.vue's existing federation.list-nodes poll.
function updateFederatedPositions(nodes: Array<{
did: string
name?: string | null
onion: string
trust_level: string
last_state?: { lat?: number | null; lon?: number | null } | null
}>) {
const next = new Map<string, FederatedNodePosition>()
for (const n of nodes) {
const lat = n.last_state?.lat
const lon = n.last_state?.lon
if (typeof lat === 'number' && typeof lon === 'number') {
next.set(n.did, {
did: n.did,
lat,
lng: lon,
name: n.name ?? null,
onion: n.onion,
trusted: n.trust_level === 'trusted',
})
}
}
federatedPositions.value = next
}
function markChatRead(contactId: number) {
viewingChatId.value = contactId
delete unreadCounts.value[contactId]
@@ -799,6 +843,8 @@ export const useMeshStore = defineStore('mesh', () => {
unreadCounts,
totalUnread,
nodePositions,
federatedPositions,
updateFederatedPositions,
deadmanStatus,
blockHeaders,
latestBlockHeight,