feat(mesh): server name in adverts + clear-all button + CI fix
- Mesh adverts now use the node's configured server name (e.g. "ThinkPad",
"Arch Dev") instead of DID key fragments ("Archy-z6MkmkSB")
- Added mesh.clear-all RPC to reset peers, messages, contacts, and history
- Added "Clear All" button in Mesh UI peers panel
- Both glibc and musl builds verified
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c02d06a66
commit
1736f6f99e
@ -319,6 +319,7 @@ impl RpcHandler {
|
||||
"mesh.send-channel-invite" => self.handle_mesh_send_channel_invite(params).await,
|
||||
"conversations.list" => self.handle_conversations_list(params).await,
|
||||
"conversations.messages" => self.handle_conversations_messages(params).await,
|
||||
"mesh.clear-all" => self.handle_mesh_clear_all().await,
|
||||
"mesh.outbox" => self.handle_mesh_outbox(params).await,
|
||||
"mesh.session-status" => self.handle_mesh_session_status(params).await,
|
||||
"mesh.rotate-prekeys" => self.handle_mesh_rotate_prekeys().await,
|
||||
|
||||
@ -215,4 +215,39 @@ impl RpcHandler {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// mesh.clear-all — Clear all peers, messages, contacts, and presence data.
|
||||
/// Resets the mesh state to a fresh start while keeping the device connected.
|
||||
pub(in crate::api::rpc) async fn handle_mesh_clear_all(&self) -> Result<serde_json::Value> {
|
||||
let service = self.mesh_service.read().await;
|
||||
if let Some(svc) = service.as_ref() {
|
||||
let state = svc.state();
|
||||
// Clear peers (except synthetic federation peers)
|
||||
state.peers.write().await.clear();
|
||||
// Clear all messages
|
||||
state.messages.write().await.clear();
|
||||
// Clear contacts
|
||||
state.contacts.write().await.clear();
|
||||
// Clear presence
|
||||
state.presence.write().await.clear();
|
||||
// Clear chunk buffer
|
||||
state.chunk_buffer.write().await.clear();
|
||||
// Re-seed federation peers
|
||||
let data_dir = self.config.data_dir.clone();
|
||||
crate::mesh::seed_federation_peers_into_mesh(state, &data_dir).await;
|
||||
// Delete persisted messages file
|
||||
let msg_file = data_dir.join("messages.json");
|
||||
let _ = tokio::fs::remove_file(&msg_file).await;
|
||||
// Delete persisted contacts file
|
||||
let contacts_file = data_dir.join("mesh-contacts.json");
|
||||
let _ = tokio::fs::remove_file(&contacts_file).await;
|
||||
// Trigger a contact refresh from the radio device
|
||||
let _ = state.send_cmd(
|
||||
crate::mesh::listener::MeshCommand::RefreshContacts,
|
||||
).await;
|
||||
Ok(serde_json::json!({ "status": "cleared" }))
|
||||
} else {
|
||||
Ok(serde_json::json!({ "status": "no_service" }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,8 @@ pub enum MeshCommand {
|
||||
/// Broadcast pre-encoded binary on a mesh channel.
|
||||
BroadcastChannel { channel: u8, payload: Vec<u8> },
|
||||
SendAdvert,
|
||||
/// Re-fetch contact list from the radio device.
|
||||
RefreshContacts,
|
||||
}
|
||||
|
||||
/// Shared state for the mesh listener, accessible from RPC handlers.
|
||||
@ -86,7 +88,7 @@ pub struct MeshState {
|
||||
/// Steganography mode for outgoing/incoming messages.
|
||||
pub stego_mode: super::steganography::SteganographyMode,
|
||||
/// Chunk reassembly buffer for multi-frame messages.
|
||||
chunk_buffer: RwLock<HashMap<(u32, u8), ChunkAssembly>>,
|
||||
pub(crate) chunk_buffer: RwLock<HashMap<(u32, u8), ChunkAssembly>>,
|
||||
/// Double Ratchet session manager for forward-secret encryption.
|
||||
pub session_manager: Arc<super::session::SessionManager>,
|
||||
/// Whether to encrypt directed relay messages (config toggle for rollback).
|
||||
@ -121,7 +123,7 @@ pub struct ContactEntry {
|
||||
}
|
||||
|
||||
/// In-progress chunk reassembly for a multi-frame message.
|
||||
struct ChunkAssembly {
|
||||
pub(crate) struct ChunkAssembly {
|
||||
chunks: HashMap<u8, String>,
|
||||
total: u8,
|
||||
created: std::time::Instant,
|
||||
@ -255,6 +257,7 @@ pub fn spawn_mesh_listener(
|
||||
our_ed_pubkey_hex: String,
|
||||
our_x25519_secret: [u8; 32],
|
||||
our_x25519_pubkey_hex: String,
|
||||
server_name: Option<String>,
|
||||
shutdown: tokio::sync::watch::Receiver<bool>,
|
||||
cmd_rx: mpsc::Receiver<MeshCommand>,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
@ -275,6 +278,7 @@ pub fn spawn_mesh_listener(
|
||||
&our_ed_pubkey_hex,
|
||||
&our_x25519_secret,
|
||||
&our_x25519_pubkey_hex,
|
||||
server_name.as_deref(),
|
||||
&mut shutdown,
|
||||
&mut cmd_rx,
|
||||
)
|
||||
|
||||
@ -249,6 +249,7 @@ pub(super) async fn run_mesh_session(
|
||||
_our_ed_pubkey_hex: &str,
|
||||
our_x25519_secret: &[u8; 32],
|
||||
_our_x25519_pubkey_hex: &str,
|
||||
server_name: Option<&str>,
|
||||
shutdown: &mut tokio::sync::watch::Receiver<bool>,
|
||||
cmd_rx: &mut mpsc::Receiver<MeshCommand>,
|
||||
) -> Result<()> {
|
||||
@ -284,9 +285,15 @@ pub(super) async fn run_mesh_session(
|
||||
|
||||
let _ = state.event_tx.send(MeshEvent::DeviceConnected(device_info));
|
||||
|
||||
// Set advert name to something identifiable
|
||||
let short_did = our_did.chars().skip(8).take(8).collect::<String>();
|
||||
let advert_name = format!("Archy-{}", short_did);
|
||||
// Set advert name to the server's human-readable name (e.g. "ThinkPad"),
|
||||
// falling back to the DID fragment if no name is configured.
|
||||
let advert_name = if let Some(name) = server_name {
|
||||
// Meshcore firmware limits advert names — truncate to 20 chars
|
||||
name.chars().take(20).collect::<String>()
|
||||
} else {
|
||||
let short_did = our_did.chars().skip(8).take(8).collect::<String>();
|
||||
format!("Archy-{}", short_did)
|
||||
};
|
||||
if let Err(e) = device.set_advert_name(&advert_name).await {
|
||||
warn!("Failed to set advert name: {}", e);
|
||||
} else {
|
||||
@ -440,5 +447,8 @@ async fn handle_send_command(
|
||||
*consecutive_write_failures = 0;
|
||||
}
|
||||
}
|
||||
MeshCommand::RefreshContacts => {
|
||||
refresh_contacts(device, state).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,6 +204,8 @@ pub struct MeshService {
|
||||
our_x25519_secret: [u8; 32],
|
||||
our_x25519_pubkey_hex: String,
|
||||
signing_key: SigningKey,
|
||||
/// Human-readable server name (e.g. "Arch Dev", "ThinkPad") for mesh adverts.
|
||||
server_name: Option<String>,
|
||||
// Phase 4: off-grid Bitcoin operations
|
||||
pub block_header_cache: Arc<BlockHeaderCache>,
|
||||
pub relay_tracker: Arc<RelayTracker>,
|
||||
@ -277,12 +279,18 @@ impl MeshService {
|
||||
our_x25519_secret: x25519_secret,
|
||||
our_x25519_pubkey_hex: x25519_pubkey_hex,
|
||||
signing_key: signing_key.clone(),
|
||||
server_name: None,
|
||||
block_header_cache,
|
||||
relay_tracker,
|
||||
dead_man_switch,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the human-readable server name used in mesh adverts.
|
||||
pub fn set_server_name(&mut self, name: Option<String>) {
|
||||
self.server_name = name;
|
||||
}
|
||||
|
||||
/// Start the background mesh listener.
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
if self.listener_handle.is_some() {
|
||||
@ -302,6 +310,7 @@ impl MeshService {
|
||||
self.our_ed_pubkey_hex.clone(),
|
||||
self.our_x25519_secret,
|
||||
self.our_x25519_pubkey_hex.clone(),
|
||||
self.server_name.clone(),
|
||||
shutdown_rx,
|
||||
cmd_rx,
|
||||
);
|
||||
@ -518,6 +527,11 @@ impl MeshService {
|
||||
self.state.status.read().await.clone()
|
||||
}
|
||||
|
||||
/// Get a reference to the shared mesh state.
|
||||
pub fn state(&self) -> &Arc<listener::MeshState> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// Get list of discovered peers.
|
||||
pub async fn peers(&self) -> Vec<MeshPeer> {
|
||||
self.state.peers.read().await.values().cloned().collect()
|
||||
|
||||
@ -156,6 +156,8 @@ impl Server {
|
||||
let signing_key = identity.signing_key();
|
||||
match crate::mesh::MeshService::new(&data_dir, signing_key, &did, &pubkey_hex).await {
|
||||
Ok(mut mesh_service) => {
|
||||
// Pass the human-readable server name for mesh adverts
|
||||
mesh_service.set_server_name(data.server_info.name.clone());
|
||||
let mut mesh_config = crate::mesh::load_config(&data_dir).await.unwrap_or_default();
|
||||
|
||||
// Auto-enable mesh if a radio is detected and no config exists yet
|
||||
|
||||
@ -229,6 +229,17 @@ async function refreshOutboxCount() {
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAllMesh() {
|
||||
if (!window.confirm('Clear all mesh peers, messages, and chat history? This cannot be undone.')) return
|
||||
try {
|
||||
await rpcClient('mesh.clear-all')
|
||||
await mesh.refreshAll()
|
||||
selectedPeer.value = null
|
||||
} catch (e) {
|
||||
console.error('Failed to clear mesh:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Off-grid Bitcoin + Dead Man's Switch
|
||||
const activeTab = ref<'chat' | 'bitcoin' | 'deadman' | 'map'>('chat')
|
||||
|
||||
@ -1341,7 +1352,10 @@ function isImageMime(mime?: string): boolean {
|
||||
|
||||
<!-- Peers list -->
|
||||
<div data-controller-container tabindex="0" class="glass-card mesh-peers-card">
|
||||
<h2 class="mesh-section-title">Peers <span class="mesh-peer-count">{{ mesh.peers.length }}</span></h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="mesh-section-title">Peers <span class="mesh-peer-count">{{ mesh.peers.length }}</span></h2>
|
||||
<button class="text-xs text-white/40 hover:text-red-400 transition-colors px-2 py-1" @click="clearAllMesh" title="Clear all peers, messages, and chat history">Clear All</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mesh.peers.length === 0 && !mesh.status?.device_connected" class="mesh-empty">
|
||||
No peers discovered yet.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user