Files
Mycell-UI/src-tauri/src/commands.rs
syoul eb86fdd182 P5: settings, persistence, polkit packaging, README
Backend
- regenerate_identity command stops the daemon, deletes
  priv_key.bin, leaves the user to restart for a fresh identity;
  falls back to the canonical XDG path when sidecar.key_path()
  isn't populated yet
- tauri.conf.json ships the polkit policy via deb.files mapping;
  src-tauri/packaging/polkit/tech.threefold.mycellium-ui.policy
  declares the spawn action with auth_admin_keep so the dialog
  appears once per session

Frontend
- config store persists SidecarConfig (peers, tunName, noTun)
  through tauri-plugin-store; App.vue reads it and forwards to
  start_daemon, replacing the hard-coded defaults
- Settings view: daemon-config form, identity panel with the
  destructive regenerate button, sidecar log viewer, About
- README rewritten end-to-end: HTTP-loopback architecture, polkit
  install path, build commands, verification matrix, and a
  honest "known limitations" section
2026-04-25 23:15:35 +02:00

299 lines
9.5 KiB
Rust

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<String>,
pub key_path: Option<String>,
pub config_path: Option<String>,
}
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<MyceliumClient> {
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<SidecarConfig>,
) -> AppResult<DaemonStatus> {
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<DaemonStatus> {
state.poller.stop();
state.sidecar.stop().await;
Ok(snapshot(&state))
}
#[tauri::command]
pub async fn node_info(state: State<'_, AppState>) -> AppResult<NodeInfo> {
let client = require_client(&state)?;
client.node_info().await
}
#[tauri::command]
pub fn sidecar_logs(state: State<'_, AppState>) -> Vec<String> {
state.sidecar.logs_snapshot()
}
// ─── Peers ───────────────────────────────────────────────────────────────────
#[tauri::command]
pub async fn peers_list(state: State<'_, AppState>) -> AppResult<Vec<PeerInfo>> {
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<AggregatedStats> {
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<RoutesSnapshot> {
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<PushMessageReceipt> {
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<PushMessageReceipt> {
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<MessageStatus> {
require_client(&state)?.message_status(&id).await
}
#[tauri::command]
pub fn inbox_messages(state: State<'_, AppState>) -> Vec<IncomingMessage> {
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<DefaultAction> {
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<Vec<String>> {
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<Vec<String>> {
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<Option<String>> {
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<PubkeyLookup> {
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<std::path::PathBuf> {
// 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<std::path::PathBuf> {
// We can't reach the AppHandle here, so we mirror Tauri's path:
// $XDG_DATA_HOME/<identifier>/ or $HOME/.local/share/<identifier>/.
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",
))
}