2.7 KiB · text History d69022d
//! devendra controller library.
//!
//! Owns the flat-file desired-state [`store`], the [`publish`] step that
//! authors it into an iroh document, and the [`web`] UI (axum + minijinja +
//! htmx, structured after ../dms). The controller is the single writer of
//! manifests and file bodies; agents write only their own status back.

use std::str::FromStr;
use std::sync::Arc;

use anyhow::{Context, Result};
use minijinja::Environment;

use devendra_node::{DocHandle, DocTicket, NamespaceId, Node, ShareMode};

pub mod adopt;
pub mod config;
pub mod publish;
pub mod store;
pub mod web;

pub use config::Config;

/// Shared state handed to every axum handler.
#[derive(Clone)]
pub struct AppState {
    pub env: Environment<'static>,
    pub node: Arc<Node>,
    /// The document agents sync from. Cheap to clone (Arc-backed handle).
    pub doc: DocHandle,
    pub config: Arc<Config>,
}

/// Open the controller's document, reusing the persisted namespace across
/// restarts so the agent join ticket stays stable. Creates and records a new
/// namespace only when none is known yet.
pub async fn open_or_create_doc(node: &Node, config: &Config) -> Result<DocHandle> {
    let ns_file = config.namespace_file();

    if let Ok(recorded) = tokio::fs::read_to_string(&ns_file).await {
        let id = NamespaceId::from_str(recorded.trim())
            .with_context(|| format!("parsing recorded namespace in {}", ns_file.display()))?;
        if let Some(doc) = node.open_doc(id).await? {
            tracing::info!(doc = %id, "reopened existing controller document");
            return Ok(doc);
        }
        tracing::warn!(
            doc = %id,
            "recorded namespace not found in store; creating a fresh document"
        );
    }

    let doc = node.create_doc().await?;
    tokio::fs::create_dir_all(&config.data_dir)
        .await
        .context("creating data dir")?;
    tokio::fs::write(&ns_file, doc.id().to_string())
        .await
        .with_context(|| format!("recording namespace to {}", ns_file.display()))?;
    tracing::info!(doc = %doc.id(), "created new controller document");
    Ok(doc)
}

/// Produce a write-capable join ticket agents use to sync. Write capability
/// lets them post their status back into the document.
///
/// The ticket is generated fresh each time (never cached) so it always carries
/// the endpoint's *current* network addresses. Stability across restarts comes
/// from the persisted secret key (stable node id) and the controller's fixed
/// iroh port (stable address) — not from freezing the ticket string, which
/// would go stale if the address ever changed.
pub async fn agent_ticket(node: &Node, doc: &DocHandle) -> Result<DocTicket> {
    node.share(doc, ShareMode::Write).await
}