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
@@ -419,6 +419,7 @@ impl RpcHandler {
// Server settings
"server.set-name" => self.handle_server_set_name(params).await,
"server.set-location" => self.handle_server_set_location(params).await,
// System monitoring
"system.get-hostname" => self.handle_system_get_hostname().await,
@@ -454,6 +454,12 @@ impl RpcHandler {
.flatten(),
};
let shared_location = if data.server_info.share_location {
data.server_info.lat.zip(data.server_info.lon)
} else {
None
};
let state = federation::build_local_state(
apps,
0.0,
@@ -467,6 +473,7 @@ impl RpcHandler {
nostr_npub,
own_fips_npub,
&federated_peers,
shared_location,
);
Ok(serde_json::to_value(&state)?)
@@ -77,6 +77,55 @@ impl RpcHandler {
}))
}
/// server.set-location — Set this node's own lat/lon + whether to share
/// it with trusted federation peers (for the Mesh Map). `lat`/`lon` are
/// optional so a caller can flip `share` off without clearing the saved
/// position, or clear the position by passing nulls.
pub(in crate::api::rpc) async fn handle_server_set_location(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let lat = params.get("lat").and_then(|v| v.as_f64());
let lon = params.get("lon").and_then(|v| v.as_f64());
let share_location = params
.get("share")
.and_then(|v| v.as_bool())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: share"))?;
if let (Some(lat), Some(lon)) = (lat, lon) {
if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
anyhow::bail!("Invalid lat/lon");
}
}
let location_file = self.config.data_dir.join("server-location.json");
let payload = serde_json::json!({ "lat": lat, "lon": lon, "share_location": share_location });
tokio::fs::write(&location_file, serde_json::to_vec(&payload)?)
.await
.context("Failed to write server location")?;
let (mut data, _) = self.state_manager.get_snapshot().await;
data.server_info.lat = lat;
data.server_info.lon = lon;
data.server_info.share_location = share_location;
self.state_manager.update_data(data).await;
info!(share_location, "Server location updated");
// Push the new location to federation peers in background, same as
// a rename — trusted peers' next state sync picks it up.
let data_dir = self.config.data_dir.clone();
let state_manager = self.state_manager.clone();
tokio::spawn(async move {
if let Err(e) = push_name_to_peers(&data_dir, &state_manager).await {
debug!("Federation location push (non-fatal): {}", e);
}
});
Ok(serde_json::json!({ "lat": lat, "lon": lon, "share_location": share_location }))
}
/// system.get-hostname — Current OS hostname + the mDNS `.local` name it
/// resolves to on the LAN (avahi-daemon advertises `<hostname>.local`).
/// Lets Settings show users where to reach this node over HTTPS for