Skip to main content

rust_supervisor/dashboard/
state.rs

1//! Dashboard state construction.
2//!
3//! The builder combines static supervisor declarations, current runtime state,
4//! and recent journal records into one payload that relay and UI can consume
5//! after session binding or reconnect.
6
7use crate::dashboard::events::{journal_to_event_records, log_record_for_event};
8use crate::dashboard::model::{
9    DashboardCriticality, DashboardState, RegistrationState, RuntimeState, SupervisorEdge,
10    SupervisorEdgeKind, SupervisorNode, SupervisorNodeKind, SupervisorTopology,
11    TargetConnectionState, TargetProcessIdentity,
12};
13use crate::id::types::SupervisorPath;
14use crate::journal::ring::EventJournal;
15use crate::spec::child::Criticality;
16use crate::spec::supervisor::SupervisorSpec;
17use crate::state::supervisor::SupervisorState;
18use std::collections::BTreeMap;
19
20/// Input required to build one dashboard state payload.
21#[derive(Debug, Clone)]
22pub struct DashboardStateInput {
23    /// Stable target process identifier.
24    pub target_id: String,
25    /// Human-readable target display name.
26    pub display_name: String,
27    /// State generation assigned by the target process.
28    pub state_generation: u64,
29    /// Number of recent records to include.
30    pub recent_limit: usize,
31}
32
33/// Builds dashboard state from current supervisor facts.
34///
35/// # Arguments
36///
37/// - `input`: Target identity and generation data.
38/// - `spec`: Supervisor declaration.
39/// - `state`: Current runtime state.
40/// - `journal`: Recent event journal.
41///
42/// # Returns
43///
44/// Returns a [`DashboardState`] ready for IPC serialization.
45pub fn build_dashboard_state(
46    input: DashboardStateInput,
47    spec: &SupervisorSpec,
48    state: &SupervisorState,
49    journal: &EventJournal,
50) -> DashboardState {
51    let config_version = spec.config_version.clone();
52    let recent_events = journal_to_event_records(
53        &input.target_id,
54        &config_version,
55        journal,
56        input.recent_limit,
57    );
58    let recent_logs = recent_events
59        .iter()
60        .map(|event| log_record_for_event(event, format!("event {}", event.event_type)))
61        .collect::<Vec<_>>();
62    DashboardState {
63        target: TargetProcessIdentity {
64            target_id: input.target_id,
65            display_name: input.display_name,
66            registration_state: RegistrationState::Active,
67            connection_state: TargetConnectionState::Registered,
68        },
69        topology: topology_from_spec(spec),
70        runtime_state: runtime_state_rows(state),
71        recent_events,
72        recent_logs,
73        dropped_event_count: journal.dropped_count,
74        dropped_log_count: 0,
75        config_version,
76        generated_at_unix_nanos: state.generated_at_unix_nanos,
77        state_generation: input.state_generation,
78    }
79}
80
81/// Builds the supervisor topology from a declaration.
82///
83/// # Arguments
84///
85/// - `spec`: Supervisor declaration.
86///
87/// # Returns
88///
89/// Returns a topology with one root and declaration-order children.
90pub fn topology_from_spec(spec: &SupervisorSpec) -> SupervisorTopology {
91    let root_path = spec.path.to_string();
92    let root = SupervisorNode {
93        node_id: root_path.clone(),
94        child_id: None,
95        path: root_path.clone(),
96        name: "root supervisor".to_owned(),
97        kind: SupervisorNodeKind::RootSupervisor,
98        tags: Vec::new(),
99        criticality: DashboardCriticality::Critical,
100        state_summary: "root".to_owned(),
101        diagnostics: BTreeMap::new(),
102    };
103    let mut nodes = vec![root.clone()];
104    let mut edges = Vec::new();
105    let mut declaration_order = vec![root_path.clone()];
106    for (index, child) in spec.children.iter().enumerate() {
107        let child_path = spec.path.join(child.id.value.clone()).to_string();
108        declaration_order.push(child_path.clone());
109        nodes.push(SupervisorNode {
110            node_id: child_path.clone(),
111            child_id: Some(child.id.to_string()),
112            path: child_path.clone(),
113            name: child.name.clone(),
114            kind: SupervisorNodeKind::ChildTask,
115            tags: child.tags.clone(),
116            criticality: criticality(child.criticality),
117            state_summary: "declared".to_owned(),
118            diagnostics: BTreeMap::new(),
119        });
120        edges.push(SupervisorEdge {
121            edge_id: format!("parent:{root_path}->{child_path}"),
122            source_path: root_path.clone(),
123            target_path: child_path.clone(),
124            kind: SupervisorEdgeKind::ParentChild,
125            order: index,
126        });
127        for (dependency_index, dependency) in child.dependencies.iter().enumerate() {
128            let dependency_path = spec.path.join(dependency.value.clone()).to_string();
129            edges.push(SupervisorEdge {
130                edge_id: format!("dependency:{dependency_path}->{child_path}"),
131                source_path: dependency_path,
132                target_path: child_path.clone(),
133                kind: SupervisorEdgeKind::Dependency,
134                order: dependency_index,
135            });
136        }
137    }
138    SupervisorTopology {
139        root,
140        nodes,
141        edges,
142        declaration_order,
143    }
144}
145
146/// Converts current supervisor state to dashboard rows.
147///
148/// # Arguments
149///
150/// - `state`: Current supervisor state.
151///
152/// # Returns
153///
154/// Returns runtime state rows sorted by child path.
155pub fn runtime_state_rows(state: &SupervisorState) -> Vec<RuntimeState> {
156    state
157        .children
158        .values()
159        .map(|child| RuntimeState {
160            child_path: child.path.to_string(),
161            lifecycle_state: child.state.as_label().to_owned(),
162            health: format!("{:?}", child.health).to_lowercase(),
163            readiness: format!("{:?}", child.readiness).to_lowercase(),
164            generation: child.generation.value,
165            attempt: child.attempt.value,
166            restart_count: child.restart_count,
167            last_failure: child
168                .last_failure
169                .as_ref()
170                .map(|failure| format!("{failure:?}")),
171            last_policy_decision: child
172                .last_policy_decision
173                .as_ref()
174                .map(|decision| decision.decision.clone()),
175            shutdown_state: format!("{:?}", state.shutdown_state).to_lowercase(),
176        })
177        .collect()
178}
179
180/// Converts child criticality to dashboard criticality.
181///
182/// # Arguments
183///
184/// - `value`: Child criticality from the supervisor declaration.
185///
186/// # Returns
187///
188/// Returns dashboard criticality.
189fn criticality(value: Criticality) -> DashboardCriticality {
190    match value {
191        Criticality::Critical => DashboardCriticality::Critical,
192        Criticality::Optional => DashboardCriticality::Standard,
193    }
194}
195
196/// Creates an empty current state for a supervisor spec.
197///
198/// # Arguments
199///
200/// - `spec`: Supervisor declaration.
201///
202/// # Returns
203///
204/// Returns a current state with declared children.
205pub fn declared_state_from_spec(spec: &SupervisorSpec) -> SupervisorState {
206    spec.children.iter().fold(
207        SupervisorState::new(
208            SupervisorPath::root(),
209            crate::event::time::EventSequence::new(1),
210            1,
211        ),
212        |state, child| {
213            let path = spec.path.join(child.id.value.clone());
214            state.with_child(crate::state::child::ChildState::declared(
215                path,
216                child.id.clone(),
217                child.name.clone(),
218            ))
219        },
220    )
221}