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
+1
View File
@@ -130,6 +130,7 @@ async function refreshFederationNodes() {
})
}
fedNodesByDid.value = next
mesh.updateFederatedPositions(res.nodes)
} catch { /* non-fatal */ }
}
async function refreshSelfOnion() {
@@ -68,6 +68,60 @@ const copiedOnion = ref(false)
const copiedDid = ref(false)
let copiedTimer: ReturnType<typeof setTimeout> | null = null
// Location sharing — opt-in only, off by default. Lets this node's own
// position appear on OTHER trusted federation peers' Mesh Map (with the
// Archy logo marker), the same way a mesh radio peer's position shows up.
// Backed by the store's serverInfo (already synced live over the WS), not
// a separate fetch, so it stays in sync with any other tab/session too.
const shareLocation = computed(() => !!store.serverInfo?.['share-location'])
const savedLat = computed(() => store.serverInfo?.lat ?? null)
const savedLon = computed(() => store.serverInfo?.lon ?? null)
const locationSaving = ref(false)
const locationError = ref('')
async function useCurrentLocation() {
locationError.value = ''
if (!navigator.geolocation) {
locationError.value = 'Geolocation not supported by this browser'
return
}
locationSaving.value = true
navigator.geolocation.getCurrentPosition(
async (pos) => {
await saveLocation(pos.coords.latitude, pos.coords.longitude, shareLocation.value)
locationSaving.value = false
},
(err) => {
locationError.value = err.code === 1 ? 'Location permission denied' : err.message
locationSaving.value = false
},
{ enableHighAccuracy: true, timeout: 15000 },
)
}
async function toggleShareLocation() {
const next = !shareLocation.value
await saveLocation(savedLat.value, savedLon.value, next)
}
async function saveLocation(lat: number | null, lon: number | null, share: boolean) {
try {
locationSaving.value = true
await rpcClient.call({ method: 'server.set-location', params: { lat, lon, share } })
// Optimistic update — the next WS state push confirms it, but no need
// to wait for that round-trip to reflect the change in the toggle/coords.
if (store.serverInfo) {
store.serverInfo.lat = lat
store.serverInfo.lon = lon
store.serverInfo['share-location'] = share
}
} catch (e) {
locationError.value = e instanceof Error ? e.message : 'Failed to save location'
} finally {
locationSaving.value = false
}
}
// mDNS hostname — HTTPS (even self-signed) is required for mic/camera access
// (getUserMedia refuses plain HTTP outside localhost); surface both so users
// know where to go for features that need it, without forcing HTTPS on anyone.
@@ -260,6 +314,41 @@ init()
</div>
</div>
<!-- Location Sharing Card — opt-in, off by default. Puts this node on
OTHER trusted peers' Mesh Map with the Archy logo marker. -->
<div class="bg-black/20 rounded-xl px-5 py-4 border border-white/10">
<div class="flex items-center gap-3 mb-2">
<svg class="w-5 h-5 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">Share Location</p>
</div>
<div class="flex items-center justify-between gap-3 mb-1">
<p class="text-sm text-white/70">Show this node on trusted peers' Mesh Map</p>
<button
class="glass-toggle"
:class="{ active: shareLocation }"
role="switch"
:aria-checked="shareLocation"
:disabled="locationSaving"
@click="toggleShareLocation"
>
<span class="glass-toggle-knob" />
</button>
</div>
<div class="flex items-center gap-2 text-xs text-white/50 mt-2">
<span v-if="savedLat !== null && savedLon !== null">{{ savedLat.toFixed(3) }}, {{ savedLon.toFixed(3) }}</span>
<span v-else class="text-white/30">No location set</span>
<button
class="ml-auto glass-button px-3 py-1 text-xs"
:disabled="locationSaving"
@click="useCurrentLocation"
>{{ locationSaving ? 'Locating' : 'Use current location' }}</button>
</div>
<p v-if="locationError" class="mt-2 text-xs text-yellow-300">{{ locationError }}</p>
</div>
<!-- Release Notes Modal -->
<Teleport to="body">
<Transition name="modal">