use crate::api::admin::NodeInfo; use crate::api::messages::{ IncomingMessage, MessageDestination, MessageStatus, PushMessageBody, PushMessageReceipt, }; use crate::api::peers::{AggregatedStats, PeerInfo}; use crate::api::pubkey::PubkeyLookup; use crate::api::routes::RoutesSnapshot; use crate::api::topics::DefaultAction; use crate::api::MyceliumClient; use crate::error::{AppError, AppResult}; use crate::sidecar::SidecarConfig; use crate::state::AppState; use serde::Serialize; use tauri::{AppHandle, State}; #[derive(Debug, Serialize)] pub struct DaemonStatus { pub running: bool, pub api_url: Option, pub key_path: Option, pub config_path: Option, } fn snapshot(state: &AppState) -> DaemonStatus { let sc = &state.sidecar; DaemonStatus { running: sc.is_running(), api_url: sc.client().map(|c| c.base_url().to_string()), key_path: sc.key_path().map(|p| p.display().to_string()), config_path: sc.config_path().map(|p| p.display().to_string()), } } fn require_client(state: &AppState) -> AppResult { state.sidecar.client().ok_or(AppError::DaemonNotRunning) } #[tauri::command] pub fn daemon_status(state: State<'_, AppState>) -> DaemonStatus { snapshot(&state) } #[tauri::command] pub async fn start_daemon( app: AppHandle, state: State<'_, AppState>, config: Option, ) -> AppResult { let cfg = config.unwrap_or_default(); state.sidecar.start(&app, &cfg).await?; state .poller .start(app.clone(), std::sync::Arc::clone(&state.sidecar)); Ok(snapshot(&state)) } #[tauri::command] pub async fn stop_daemon(state: State<'_, AppState>) -> AppResult { state.poller.stop(); state.sidecar.stop().await; Ok(snapshot(&state)) } #[tauri::command] pub async fn node_info(state: State<'_, AppState>) -> AppResult { let client = require_client(&state)?; client.node_info().await } #[tauri::command] pub fn sidecar_logs(state: State<'_, AppState>) -> Vec { state.sidecar.logs_snapshot() } // ─── Peers ─────────────────────────────────────────────────────────────────── #[tauri::command] pub async fn peers_list(state: State<'_, AppState>) -> AppResult> { require_client(&state)?.list_peers().await } #[tauri::command] pub async fn peer_add(state: State<'_, AppState>, endpoint: String) -> AppResult<()> { if endpoint.trim().is_empty() { return Err(AppError::BadInput("endpoint must not be empty".into())); } require_client(&state)?.add_peer(endpoint.trim()).await } #[tauri::command] pub async fn peer_remove(state: State<'_, AppState>, endpoint: String) -> AppResult<()> { require_client(&state)?.remove_peer(&endpoint).await } #[tauri::command] pub async fn peers_stats(state: State<'_, AppState>) -> AppResult { let peers = require_client(&state)?.list_peers().await?; Ok(crate::api::peers::aggregate(&peers)) } // ─── Routes ────────────────────────────────────────────────────────────────── #[tauri::command] pub async fn routes_snapshot(state: State<'_, AppState>) -> AppResult { require_client(&state)?.routes_snapshot().await } // ─── Messages ──────────────────────────────────────────────────────────────── #[tauri::command] pub async fn send_message( state: State<'_, AppState>, destination: MessageDestination, topic_b64: String, payload_b64: String, ) -> AppResult { let body = PushMessageBody { dst: destination, topic: topic_b64, payload: payload_b64, }; require_client(&state)?.send_message(&body).await } #[tauri::command] pub async fn reply_message( state: State<'_, AppState>, id: String, destination: MessageDestination, topic_b64: String, payload_b64: String, ) -> AppResult { let body = PushMessageBody { dst: destination, topic: topic_b64, payload: payload_b64, }; require_client(&state)?.reply_message(&id, &body).await } #[tauri::command] pub async fn message_status(state: State<'_, AppState>, id: String) -> AppResult { require_client(&state)?.message_status(&id).await } #[tauri::command] pub fn inbox_messages(state: State<'_, AppState>) -> Vec { state.poller.inbox_snapshot() } #[tauri::command] pub fn inbox_clear(state: State<'_, AppState>) { state.poller.clear_inbox(); } // ─── Topics ────────────────────────────────────────────────────────────────── #[tauri::command] pub async fn topics_default_get(state: State<'_, AppState>) -> AppResult { require_client(&state)?.topics_default_get().await } #[tauri::command] pub async fn topics_default_set(state: State<'_, AppState>, accept: bool) -> AppResult<()> { require_client(&state)? .topics_default_set(&DefaultAction { accept }) .await } #[tauri::command] pub async fn topics_list(state: State<'_, AppState>) -> AppResult> { require_client(&state)?.topics_list().await } #[tauri::command] pub async fn topic_add(state: State<'_, AppState>, topic_b64: String) -> AppResult<()> { if topic_b64.trim().is_empty() { return Err(AppError::BadInput("topic must not be empty".into())); } require_client(&state)?.topic_add(topic_b64.trim()).await } #[tauri::command] pub async fn topic_remove(state: State<'_, AppState>, topic_b64: String) -> AppResult<()> { require_client(&state)?.topic_remove(&topic_b64).await } #[tauri::command] pub async fn topic_sources_list( state: State<'_, AppState>, topic_b64: String, ) -> AppResult> { require_client(&state)?.topic_sources_list(&topic_b64).await } #[tauri::command] pub async fn topic_source_add( state: State<'_, AppState>, topic_b64: String, subnet: String, ) -> AppResult<()> { require_client(&state)? .topic_source_add(&topic_b64, &subnet) .await } #[tauri::command] pub async fn topic_source_remove( state: State<'_, AppState>, topic_b64: String, subnet: String, ) -> AppResult<()> { require_client(&state)? .topic_source_remove(&topic_b64, &subnet) .await } #[tauri::command] pub async fn topic_forward_get( state: State<'_, AppState>, topic_b64: String, ) -> AppResult> { require_client(&state)?.topic_forward_get(&topic_b64).await } #[tauri::command] pub async fn topic_forward_set( state: State<'_, AppState>, topic_b64: String, socket_path: String, ) -> AppResult<()> { require_client(&state)? .topic_forward_set(&topic_b64, &socket_path) .await } #[tauri::command] pub async fn topic_forward_remove(state: State<'_, AppState>, topic_b64: String) -> AppResult<()> { require_client(&state)? .topic_forward_remove(&topic_b64) .await } // ─── Pubkey ────────────────────────────────────────────────────────────────── #[tauri::command] pub async fn lookup_pubkey(state: State<'_, AppState>, ip: String) -> AppResult { require_client(&state)?.lookup_pubkey(&ip).await } // ─── Identity ──────────────────────────────────────────────────────────────── /// Stops the daemon (if running), removes the saved private key file, and /// returns the daemon to the idle state. The caller restarts the daemon /// to provoke regeneration. #[tauri::command] pub async fn regenerate_identity(state: State<'_, AppState>) -> AppResult<()> { state.poller.stop(); let key_path = state.sidecar.key_path(); state.sidecar.stop().await; if let Some(path) = key_path { if path.exists() { std::fs::remove_file(&path).map_err(AppError::from)?; } } else { // Sidecar never started; resolve the canonical path via app_data_dir. if let Some(p) = default_key_path() { if p.exists() { std::fs::remove_file(&p).map_err(AppError::from)?; } } } Ok(()) } fn default_key_path() -> Option { // Best-effort: fall back to the same XDG location the sidecar uses. dirs_like_app_data().ok().map(|d| d.join("priv_key.bin")) } fn dirs_like_app_data() -> std::io::Result { // We can't reach the AppHandle here, so we mirror Tauri's path: // $XDG_DATA_HOME// or $HOME/.local/share//. let identifier = "tech.threefold.mycellium-ui"; if let Ok(d) = std::env::var("XDG_DATA_HOME") { return Ok(std::path::PathBuf::from(d).join(identifier)); } if let Ok(home) = std::env::var("HOME") { return Ok(std::path::PathBuf::from(home) .join(".local/share") .join(identifier)); } Err(std::io::Error::new( std::io::ErrorKind::NotFound, "no app data dir", )) }