Skip to main content

rust_supervisor/dashboard/
model.rs

1//! Shared dashboard data model.
2//!
3//! These structs are the JSON contract shared by target IPC, relay, and the
4//! dashboard UI. They intentionally use owned values so callers can serialize,
5//! clone, and test messages without borrowing runtime internals.
6
7use crate::control::command::{CommandResult, CurrentState};
8use crate::control::outcome::{
9    ChildAttemptStatus, ChildControlFailure, ChildControlFailurePhase, ChildControlOperation,
10    ChildControlResult as RuntimeChildControlResult, ChildLivenessState, ChildRuntimeRecord,
11    ChildStopState, GenerationFenceDecision, GenerationFenceOutcome, GenerationFencePhase,
12    PendingRestartSummary, RestartLimitState,
13};
14use crate::readiness::signal::ReadinessState;
15use schemars::JsonSchema;
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17use serde_json::Value;
18use std::collections::BTreeMap;
19
20/// Serializes a u128 nanosecond timestamp as a string to preserve
21/// precision across JSON boundaries (JavaScript Number loses precision
22/// above 2^53).
23pub fn serialize_nanos<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
24where
25    S: Serializer,
26{
27    serializer.serialize_str(&value.to_string())
28}
29
30/// Deserializes a u128 nanosecond timestamp from a string or number.
31pub fn deserialize_nanos<'de, D>(deserializer: D) -> Result<u128, D::Error>
32where
33    D: Deserializer<'de>,
34{
35    use serde::de::Error;
36
37    struct NanosVisitor;
38    impl<'de> serde::de::Visitor<'de> for NanosVisitor {
39        type Value = u128;
40
41        /// Describes the accepted timestamp representation.
42        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43            formatter.write_str("a nanosecond timestamp as string or integer")
44        }
45
46        /// Parses a timestamp from a string.
47        fn visit_str<E: Error>(self, s: &str) -> Result<u128, E> {
48            s.parse::<u128>()
49                .map_err(|e| E::custom(format!("invalid nanos string: {e}")))
50        }
51
52        /// Converts an unsigned 64-bit timestamp.
53        fn visit_u64<E: Error>(self, v: u64) -> Result<u128, E> {
54            Ok(v as u128)
55        }
56
57        /// Accepts an unsigned 128-bit timestamp.
58        fn visit_u128<E: Error>(self, v: u128) -> Result<u128, E> {
59            Ok(v)
60        }
61
62        /// Converts a signed 64-bit timestamp when it is non-negative.
63        fn visit_i64<E: Error>(self, v: i64) -> Result<u128, E> {
64            u128::try_from(v).map_err(|_| E::custom("negative timestamp is not valid for nanos"))
65        }
66    }
67
68    deserializer.deserialize_any(NanosVisitor)
69}
70
71/// Serializes an `Option<u128>` nanosecond timestamp as an optional string.
72pub fn serialize_nanos_opt<S>(value: &Option<u128>, serializer: S) -> Result<S::Ok, S::Error>
73where
74    S: Serializer,
75{
76    match value {
77        Some(v) => serializer.serialize_str(&v.to_string()),
78        None => serializer.serialize_none(),
79    }
80}
81
82/// Supported command metadata sent to the relay.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84pub struct SupportedCommand {
85    /// Wire command name.
86    pub name: String,
87    /// Whether the command can be retried with the same command identifier.
88    pub idempotent: bool,
89    /// Command timeout in seconds.
90    pub timeout_seconds: u64,
91}
92
93/// Target process registration payload sent to the relay.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
95pub struct TargetProcessRegistration {
96    /// Stable target process identifier.
97    pub target_id: String,
98    /// Human-readable display name.
99    pub display_name: String,
100    /// Local Unix domain socket path exposed by the target.
101    pub ipc_path: String,
102    /// Lease duration in seconds.
103    pub lease_seconds: u64,
104    /// Commands supported by this target.
105    pub supported_commands: Vec<SupportedCommand>,
106}
107
108/// Current registration state for a target process.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
110#[serde(rename_all = "snake_case")]
111pub enum RegistrationState {
112    /// Registration was accepted and is visible.
113    Active,
114    /// Registration was rejected.
115    Rejected,
116    /// Registration lease expired.
117    Expired,
118}
119
120/// Current relay connection state for a target process.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
122#[serde(rename_all = "snake_case")]
123pub enum TargetConnectionState {
124    /// Target is registered but no session has bound it.
125    Registered,
126    /// Relay is connecting to target IPC.
127    Connecting,
128    /// Relay is connected to target IPC.
129    Connected,
130    /// Relay is reconnecting to target IPC.
131    Reconnecting,
132    /// Target IPC is unavailable.
133    Unavailable,
134    /// Registration lease expired.
135    Expired,
136}
137
138/// Target identity shown in dashboard state payloads and target lists.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
140pub struct TargetProcessIdentity {
141    /// Stable target process identifier.
142    pub target_id: String,
143    /// Human-readable display name.
144    pub display_name: String,
145    /// Current registration state.
146    pub registration_state: RegistrationState,
147    /// Current relay connection state.
148    pub connection_state: TargetConnectionState,
149}
150
151/// Complete dashboard state returned when a target is opened or reconnected.
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
153pub struct DashboardState {
154    /// Target process identity.
155    pub target: TargetProcessIdentity,
156    /// Supervisor topology.
157    pub topology: SupervisorTopology,
158    /// Runtime state rows indexed by child path.
159    pub runtime_state: Vec<RuntimeState>,
160    /// Runtime records returned by the control loop current state.
161    pub child_runtime_records: Vec<DashboardChildRuntimeRecord>,
162    /// Recent events retained by the target.
163    pub recent_events: Vec<EventRecord>,
164    /// Recent logs retained by the target.
165    pub recent_logs: Vec<LogRecord>,
166    /// Number of dropped events.
167    pub dropped_event_count: u64,
168    /// Number of dropped logs.
169    pub dropped_log_count: u64,
170    /// Configuration version string.
171    pub config_version: String,
172    /// Generated time as Unix nanoseconds.
173    #[serde(serialize_with = "serialize_nanos")]
174    pub generated_at_unix_nanos: u128,
175    /// Monotonic state generation for this target.
176    pub state_generation: u64,
177}
178
179/// Supervisor graph for dashboard rendering.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
181pub struct SupervisorTopology {
182    /// Root supervisor node.
183    pub root: SupervisorNode,
184    /// All visible nodes including the root.
185    pub nodes: Vec<SupervisorNode>,
186    /// Parent-child and dependency edges.
187    pub edges: Vec<SupervisorEdge>,
188    /// Node paths in declaration order.
189    pub declaration_order: Vec<String>,
190}
191
192/// Node kind visible in the topology.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
194#[serde(rename_all = "snake_case")]
195pub enum SupervisorNodeKind {
196    /// Root supervisor node.
197    RootSupervisor,
198    /// Child task node.
199    ChildTask,
200}
201
202/// Criticality shown by dashboard nodes.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum DashboardCriticality {
206    /// Critical child.
207    Critical,
208    /// Standard child.
209    Standard,
210    /// Best-effort child.
211    BestEffort,
212}
213
214/// Node displayed in the supervisor topology.
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
216pub struct SupervisorNode {
217    /// Stable node identifier.
218    pub node_id: String,
219    /// Optional child identifier.
220    pub child_id: Option<String>,
221    /// Absolute child path.
222    pub path: String,
223    /// Human-readable node name.
224    pub name: String,
225    /// Node kind.
226    pub kind: SupervisorNodeKind,
227    /// Low-cardinality tags.
228    pub tags: Vec<String>,
229    /// Node criticality.
230    pub criticality: DashboardCriticality,
231    /// Current state summary.
232    pub state_summary: String,
233    /// Key diagnostic fields.
234    pub diagnostics: BTreeMap<String, String>,
235}
236
237/// Edge kind visible in the topology.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
239#[serde(rename_all = "snake_case")]
240pub enum SupervisorEdgeKind {
241    /// Parent-child edge.
242    ParentChild,
243    /// Dependency edge.
244    Dependency,
245}
246
247/// Edge displayed in the supervisor topology.
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
249pub struct SupervisorEdge {
250    /// Stable edge identifier.
251    pub edge_id: String,
252    /// Source node path.
253    pub source_path: String,
254    /// Target node path.
255    pub target_path: String,
256    /// Edge kind.
257    pub kind: SupervisorEdgeKind,
258    /// Declaration or dependency order.
259    pub order: usize,
260}
261
262/// Runtime state shown for one child.
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
264pub struct RuntimeState {
265    /// Child path.
266    pub child_path: String,
267    /// Lifecycle state label.
268    pub lifecycle_state: String,
269    /// Health status label.
270    pub health: String,
271    /// Readiness status label.
272    pub readiness: String,
273    /// Child generation.
274    pub generation: u64,
275    /// Child child_start_count.
276    pub child_start_count: u64,
277    /// Restart count.
278    pub restart_count: u64,
279    /// Optional last failure summary.
280    pub last_failure: Option<String>,
281    /// Optional last policy decision summary.
282    pub last_policy_decision: Option<String>,
283    /// Supervisor shutdown state label.
284    pub shutdown_state: String,
285}
286
287/// Managed child state derived for dashboard display.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
289#[serde(rename_all = "snake_case")]
290pub enum DashboardManagedChildState {
291    /// Child is active and should be displayed as running.
292    Running,
293    /// Child is paused.
294    Paused,
295    /// Child is quarantined.
296    Quarantined,
297    /// Child is removed.
298    Removed,
299}
300
301impl From<ChildControlOperation> for DashboardManagedChildState {
302    /// Converts a runtime control operation into a dashboard managed state.
303    fn from(value: ChildControlOperation) -> Self {
304        match value {
305            ChildControlOperation::Active => Self::Running,
306            ChildControlOperation::Paused => Self::Paused,
307            ChildControlOperation::Quarantined => Self::Quarantined,
308            ChildControlOperation::Removed => Self::Removed,
309        }
310    }
311}
312
313impl DashboardManagedChildState {
314    /// Returns the stable dashboard label.
315    ///
316    /// # Arguments
317    ///
318    /// This function has no arguments.
319    ///
320    /// # Returns
321    ///
322    /// Returns the lifecycle label used by runtime rows.
323    pub fn as_label(&self) -> &'static str {
324        match self {
325            Self::Running => "running",
326            Self::Paused => "paused",
327            Self::Quarantined => "quarantined",
328            Self::Removed => "removed",
329        }
330    }
331}
332
333/// Child control operation label for dashboard payloads.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
335#[serde(rename_all = "snake_case")]
336pub enum DashboardChildControlOperation {
337    /// Runtime state remains active.
338    Active,
339    /// Runtime state is paused.
340    Paused,
341    /// Runtime state is quarantined.
342    Quarantined,
343    /// Runtime state is removed or waiting for removal.
344    Removed,
345}
346
347impl From<ChildControlOperation> for DashboardChildControlOperation {
348    /// Converts a runtime control operation into a dashboard operation.
349    fn from(value: ChildControlOperation) -> Self {
350        match value {
351            ChildControlOperation::Active => Self::Active,
352            ChildControlOperation::Paused => Self::Paused,
353            ChildControlOperation::Quarantined => Self::Quarantined,
354            ChildControlOperation::Removed => Self::Removed,
355        }
356    }
357}
358
359/// Attempt status label for dashboard payloads.
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
361#[serde(rename_all = "snake_case")]
362pub enum DashboardChildAttemptStatus {
363    /// Child attempt is starting.
364    Starting,
365    /// Child attempt is running.
366    Running,
367    /// Child attempt is ready.
368    Ready,
369    /// Child attempt is cancelling.
370    Cancelling,
371    /// Child attempt has stopped.
372    Stopped,
373}
374
375impl From<ChildAttemptStatus> for DashboardChildAttemptStatus {
376    /// Converts a runtime attempt status into a dashboard attempt status.
377    fn from(value: ChildAttemptStatus) -> Self {
378        match value {
379            ChildAttemptStatus::Starting => Self::Starting,
380            ChildAttemptStatus::Running => Self::Running,
381            ChildAttemptStatus::Ready => Self::Ready,
382            ChildAttemptStatus::Cancelling => Self::Cancelling,
383            ChildAttemptStatus::Stopped => Self::Stopped,
384        }
385    }
386}
387
388/// Stop state label for dashboard payloads.
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
390#[serde(rename_all = "snake_case")]
391pub enum DashboardChildStopState {
392    /// No stop action is in progress.
393    Idle,
394    /// No active attempt exists.
395    NoActiveAttempt,
396    /// Cancellation was delivered.
397    CancelDelivered,
398    /// Stop completed.
399    Completed,
400    /// Stop failed.
401    Failed,
402}
403
404impl From<ChildStopState> for DashboardChildStopState {
405    /// Converts a runtime stop state into a dashboard stop state.
406    fn from(value: ChildStopState) -> Self {
407        match value {
408            ChildStopState::Idle => Self::Idle,
409            ChildStopState::NoActiveAttempt => Self::NoActiveAttempt,
410            ChildStopState::CancelDelivered => Self::CancelDelivered,
411            ChildStopState::Completed => Self::Completed,
412            ChildStopState::Failed => Self::Failed,
413        }
414    }
415}
416
417/// Readiness state label for dashboard payloads.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
419#[serde(rename_all = "snake_case")]
420pub enum DashboardReadinessState {
421    /// Readiness has not been reported.
422    Unreported,
423    /// Child reported readiness.
424    Ready,
425    /// Child reported that it is not ready.
426    NotReady,
427}
428
429impl From<ReadinessState> for DashboardReadinessState {
430    /// Converts a runtime readiness state into a dashboard readiness state.
431    fn from(value: ReadinessState) -> Self {
432        match value {
433            ReadinessState::Unreported => Self::Unreported,
434            ReadinessState::Ready => Self::Ready,
435            ReadinessState::NotReady => Self::NotReady,
436        }
437    }
438}
439
440impl DashboardReadinessState {
441    /// Returns the stable dashboard label.
442    ///
443    /// # Arguments
444    ///
445    /// This function has no arguments.
446    ///
447    /// # Returns
448    ///
449    /// Returns the readiness label used by runtime rows.
450    pub fn as_label(&self) -> &'static str {
451        match self {
452            Self::Unreported => "unreported",
453            Self::Ready => "ready",
454            Self::NotReady => "not_ready",
455        }
456    }
457}
458
459/// Failure phase label for dashboard payloads.
460#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
461#[serde(rename_all = "snake_case")]
462pub enum DashboardChildControlFailurePhase {
463    /// Waiting for completion failed.
464    WaitCompletion,
465}
466
467impl From<ChildControlFailurePhase> for DashboardChildControlFailurePhase {
468    /// Converts a runtime failure phase into a dashboard failure phase.
469    fn from(value: ChildControlFailurePhase) -> Self {
470        match value {
471            ChildControlFailurePhase::WaitCompletion => Self::WaitCompletion,
472        }
473    }
474}
475
476/// Liveness facts shown by dashboard runtime records.
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
478pub struct DashboardChildLivenessState {
479    /// Last heartbeat as Unix nanoseconds (string in JSON).
480    #[serde(
481        default,
482        skip_serializing_if = "Option::is_none",
483        serialize_with = "serialize_nanos_opt"
484    )]
485    /// Last heartbeat Unix timestamp in nanoseconds, serialized as a
486    /// JSON string to preserve precision across JavaScript boundaries.
487    pub last_heartbeat_at_unix_nanos: Option<u128>,
488    /// Whether the heartbeat is stale.
489    pub heartbeat_stale: bool,
490    /// Last observed readiness state.
491    pub readiness: DashboardReadinessState,
492}
493
494impl DashboardChildLivenessState {
495    /// Converts runtime liveness into a dashboard liveness model.
496    ///
497    /// # Arguments
498    ///
499    /// - `value`: Runtime liveness state.
500    ///
501    /// # Returns
502    ///
503    /// Returns a dashboard liveness state.
504    pub fn from_liveness(value: &ChildLivenessState) -> Self {
505        Self {
506            last_heartbeat_at_unix_nanos: value.last_heartbeat_at_unix_nanos,
507            heartbeat_stale: value.heartbeat_stale,
508            readiness: DashboardReadinessState::from(value.readiness),
509        }
510    }
511}
512
513/// Restart limit facts shown by dashboard runtime records.
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
515pub struct DashboardRestartLimitState {
516    /// Restart accounting window in milliseconds.
517    #[serde(serialize_with = "serialize_nanos")]
518    pub window_millis: u128,
519    /// Restart limit inside the window.
520    pub limit: u32,
521    /// Restart count used so far.
522    pub used: u32,
523    /// Remaining restart count.
524    pub remaining: u32,
525    /// Whether the restart limit is exhausted.
526    pub exhausted: bool,
527    /// Last update timestamp in Unix nanoseconds.
528    #[serde(serialize_with = "serialize_nanos")]
529    pub updated_at_unix_nanos: u128,
530}
531
532impl DashboardRestartLimitState {
533    /// Converts runtime restart limit into a dashboard restart limit model.
534    ///
535    /// # Arguments
536    ///
537    /// - `value`: Runtime restart limit state.
538    ///
539    /// # Returns
540    ///
541    /// Returns a dashboard restart limit state.
542    pub fn from_restart_limit(value: &RestartLimitState) -> Self {
543        Self {
544            window_millis: value.window.as_millis(),
545            limit: value.limit,
546            used: value.used,
547            remaining: value.remaining,
548            exhausted: value.exhausted,
549            updated_at_unix_nanos: value.updated_at_unix_nanos,
550        }
551    }
552}
553
554/// Structured control failure shown by dashboard payloads.
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
556pub struct DashboardChildControlFailure {
557    /// Failure phase.
558    pub phase: DashboardChildControlFailurePhase,
559    /// Human-readable failure reason.
560    pub reason: String,
561    /// Whether callers can retry.
562    pub recoverable: bool,
563}
564
565impl DashboardChildControlFailure {
566    /// Converts a runtime control failure into a dashboard failure model.
567    ///
568    /// # Arguments
569    ///
570    /// - `value`: Runtime control failure.
571    ///
572    /// # Returns
573    ///
574    /// Returns a dashboard control failure.
575    pub fn from_failure(value: &ChildControlFailure) -> Self {
576        Self {
577            phase: DashboardChildControlFailurePhase::from(value.phase),
578            reason: value.reason.clone(),
579            recoverable: value.recoverable,
580        }
581    }
582}
583
584/// Dashboard projection for generation fencing phases.
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
586#[serde(rename_all = "snake_case")]
587pub enum DashboardGenerationFencePhase {
588    /// No fencing wait state.
589    Open,
590    /// Restart accepted; waiting for the old attempt to stop.
591    WaitingForOldStop,
592    /// Abort escalated against the outdated attempt.
593    AbortingOld,
594    /// Fence cleared; eligible to spawn the queued generation when other gates permit.
595    ReadyToStart,
596    /// Record removed or fencing closed due to supervisor shutdown semantics.
597    Closed,
598}
599
600impl From<GenerationFencePhase> for DashboardGenerationFencePhase {
601    /// Mirrors the runtime enumeration into dashboard JSON payloads.
602    fn from(value: GenerationFencePhase) -> Self {
603        match value {
604            GenerationFencePhase::Open => Self::Open,
605            GenerationFencePhase::WaitingForOldStop => Self::WaitingForOldStop,
606            GenerationFencePhase::AbortingOld => Self::AbortingOld,
607            GenerationFencePhase::ReadyToStart => Self::ReadyToStart,
608            GenerationFencePhase::Closed => Self::Closed,
609        }
610    }
611}
612
613/// Dashboard projection for generation fencing decisions tied to restart commands.
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
615#[serde(rename_all = "snake_case")]
616pub enum DashboardGenerationFenceDecision {
617    /// Immediately spawned due to lacking an active attempt.
618    StartedImmediately,
619    /// Queued until an active attempt observes stop semantics.
620    QueuedAfterStop,
621    /// Duplicate restart merged onto the existing pending fence request.
622    AlreadyPending,
623    /// Supervisor shutdown prevents further restart progress.
624    BlockedByShutdown,
625    /// Structured rejection surfaced through dashboard conflicts.
626    Rejected,
627}
628
629impl From<GenerationFenceDecision> for DashboardGenerationFenceDecision {
630    /// Maps runtime restart fence decisions onto dashboard labels.
631    fn from(value: GenerationFenceDecision) -> Self {
632        match value {
633            GenerationFenceDecision::StartedImmediately => Self::StartedImmediately,
634            GenerationFenceDecision::QueuedAfterStop => Self::QueuedAfterStop,
635            GenerationFenceDecision::AlreadyPending => Self::AlreadyPending,
636            GenerationFenceDecision::BlockedByShutdown => Self::BlockedByShutdown,
637            GenerationFenceDecision::Rejected => Self::Rejected,
638        }
639    }
640}
641
642/// Dashboard projection covering optional pending restart fingerprints.
643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
644pub struct DashboardPendingRestartSummary {
645    /// Old generation locked during the queued restart handshake.
646    pub old_generation: u64,
647    /// Old attempt locked during the queued restart handshake.
648    pub old_attempt: u64,
649    /// Target generation that should start after cleanup completes.
650    pub target_generation: u64,
651}
652
653impl From<&PendingRestartSummary> for DashboardPendingRestartSummary {
654    /// Converts numeric runtime identifiers into wire-friendly dashboard fields.
655    fn from(value: &PendingRestartSummary) -> Self {
656        Self {
657            old_generation: value.old_generation.value,
658            old_attempt: value.old_attempt.value,
659            target_generation: value.target_generation.value,
660        }
661    }
662}
663
664/// Dashboard projection of structured generation fencing command outcomes.
665#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
666pub struct DashboardGenerationFenceOutcome {
667    /// High-level fencing decision surfaced to operators.
668    pub decision: DashboardGenerationFenceDecision,
669    /// Serialized old generation counterpart when meaningful.
670    pub old_generation: Option<u64>,
671    /// Serialized old attempt counterpart when meaningful.
672    pub old_attempt: Option<u64>,
673    /// Serialized queued target generation counterpart when meaningful.
674    pub target_generation: Option<u64>,
675    /// Mirrors runtime cancel delivery semantics during restart flows.
676    pub cancel_delivered: bool,
677    /// Mirrors runtime abort delivery semantics during restart flows.
678    pub abort_requested: bool,
679    /// Optional structured failure propagated when decisions reject restart work.
680    pub conflict: Option<DashboardChildControlFailure>,
681}
682
683impl From<&GenerationFenceOutcome> for DashboardGenerationFenceOutcome {
684    /// Converts nested runtime payloads into serialized dashboard equivalents.
685    fn from(outcome: &GenerationFenceOutcome) -> Self {
686        Self {
687            decision: outcome.decision.into(),
688            old_generation: outcome.old_generation.map(|generation| generation.value),
689            old_attempt: outcome.old_attempt.map(|attempt| attempt.value),
690            target_generation: outcome.target_generation.map(|generation| generation.value),
691            cancel_delivered: outcome.cancel_delivered,
692            abort_requested: outcome.abort_requested,
693            conflict: outcome
694                .conflict
695                .as_ref()
696                .map(DashboardChildControlFailure::from_failure),
697        }
698    }
699}
700
701/// Dashboard projection of one child runtime record.
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
703pub struct DashboardChildRuntimeRecord {
704    /// Stable child identifier.
705    pub child_id: String,
706    /// Child path in the supervisor tree.
707    pub child_path: String,
708    /// Current active generation.
709    pub generation: Option<u64>,
710    /// Current active attempt.
711    pub attempt: Option<u64>,
712    /// Current attempt status.
713    pub status: Option<DashboardChildAttemptStatus>,
714    /// Current control operation.
715    pub operation: DashboardChildControlOperation,
716    /// Managed child state derived from operation.
717    pub managed_child_state: DashboardManagedChildState,
718    /// Current liveness state.
719    pub liveness: DashboardChildLivenessState,
720    /// Current restart limit state.
721    pub restart_limit: DashboardRestartLimitState,
722    /// Current stop progress.
723    pub stop_state: DashboardChildStopState,
724    /// Most recent control failure.
725    pub failure: Option<DashboardChildControlFailure>,
726    /// Generation fencing phase mirroring runtime projection.
727    pub generation_fence_phase: DashboardGenerationFencePhase,
728    /// Pending restart summary when the runtime still waits on the old attempt.
729    pub pending_restart: Option<DashboardPendingRestartSummary>,
730}
731
732impl DashboardChildRuntimeRecord {
733    /// Converts a runtime record into a dashboard runtime record.
734    ///
735    /// # Arguments
736    ///
737    /// - `record`: Runtime child record returned by current state.
738    ///
739    /// # Returns
740    ///
741    /// Returns a dashboard runtime record.
742    pub fn from_runtime_record(record: &ChildRuntimeRecord) -> Self {
743        Self {
744            child_id: record.child_id.to_string(),
745            child_path: record.path.to_string(),
746            generation: record.generation.map(|generation| generation.value),
747            attempt: record.attempt.map(|attempt| attempt.value),
748            status: record.status.map(DashboardChildAttemptStatus::from),
749            operation: DashboardChildControlOperation::from(record.operation),
750            managed_child_state: DashboardManagedChildState::from(record.operation),
751            liveness: DashboardChildLivenessState::from_liveness(&record.liveness),
752            restart_limit: DashboardRestartLimitState::from_restart_limit(&record.restart_limit),
753            stop_state: DashboardChildStopState::from(record.stop_state),
754            failure: record
755                .failure
756                .as_ref()
757                .map(DashboardChildControlFailure::from_failure),
758            generation_fence_phase: record.generation_fence_phase.into(),
759            pending_restart: record
760                .pending_restart
761                .as_ref()
762                .map(DashboardPendingRestartSummary::from),
763        }
764    }
765}
766
767/// Dashboard projection of a runtime current state result.
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
769pub struct DashboardCurrentState {
770    /// Number of children known to the control loop.
771    pub child_count: usize,
772    /// Whether tree shutdown has completed.
773    pub shutdown_completed: bool,
774    /// Runtime state records for declared children.
775    pub child_runtime_records: Vec<DashboardChildRuntimeRecord>,
776}
777
778impl DashboardCurrentState {
779    /// Converts a runtime current state into a dashboard current state.
780    ///
781    /// # Arguments
782    ///
783    /// - `state`: Runtime current state.
784    ///
785    /// # Returns
786    ///
787    /// Returns a dashboard current state.
788    pub fn from_current_state(state: &CurrentState) -> Self {
789        Self {
790            child_count: state.child_count,
791            shutdown_completed: state.shutdown_completed,
792            child_runtime_records: state
793                .child_runtime_records
794                .iter()
795                .map(DashboardChildRuntimeRecord::from_runtime_record)
796                .collect(),
797        }
798    }
799}
800
801/// Dashboard projection of a child control command result.
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
803pub struct DashboardChildControlResult {
804    /// Stable child identifier.
805    pub child_id: String,
806    /// Active attempt targeted by the command.
807    pub attempt: Option<u64>,
808    /// Active generation targeted by the command.
809    pub generation: Option<u64>,
810    /// Control operation before command handling.
811    pub operation_before: DashboardChildControlOperation,
812    /// Control operation after command handling.
813    pub operation_after: DashboardChildControlOperation,
814    /// Managed child state before command handling.
815    pub managed_child_state_before: DashboardManagedChildState,
816    /// Managed child state after command handling.
817    pub managed_child_state_after: DashboardManagedChildState,
818    /// Current attempt status.
819    pub status: Option<DashboardChildAttemptStatus>,
820    /// Whether this command delivered cancellation.
821    pub cancel_delivered: bool,
822    /// Stop progress after command handling.
823    pub stop_state: DashboardChildStopState,
824    /// Current restart limit state.
825    pub restart_limit: DashboardRestartLimitState,
826    /// Current liveness state.
827    pub liveness: DashboardChildLivenessState,
828    /// Whether this command reused existing state idempotently.
829    pub idempotent: bool,
830    /// Current failure reason.
831    pub failure: Option<DashboardChildControlFailure>,
832    /// Optional serialized generation fence outcome for restart-style commands.
833    pub generation_fence: Option<DashboardGenerationFenceOutcome>,
834}
835
836impl DashboardChildControlResult {
837    /// Converts a runtime child control result into a dashboard control result.
838    ///
839    /// # Arguments
840    ///
841    /// - `outcome`: Runtime child control result.
842    ///
843    /// # Returns
844    ///
845    /// Returns a dashboard child control result.
846    pub fn from_child_control_result(outcome: &RuntimeChildControlResult) -> Self {
847        Self {
848            child_id: outcome.child_id.to_string(),
849            attempt: outcome.attempt.map(|attempt| attempt.value),
850            generation: outcome.generation.map(|generation| generation.value),
851            operation_before: DashboardChildControlOperation::from(outcome.operation_before),
852            operation_after: DashboardChildControlOperation::from(outcome.operation_after),
853            managed_child_state_before: DashboardManagedChildState::from(outcome.operation_before),
854            managed_child_state_after: DashboardManagedChildState::from(outcome.operation_after),
855            status: outcome.status.map(DashboardChildAttemptStatus::from),
856            cancel_delivered: outcome.cancel_delivered,
857            stop_state: DashboardChildStopState::from(outcome.stop_state),
858            restart_limit: DashboardRestartLimitState::from_restart_limit(&outcome.restart_limit),
859            liveness: DashboardChildLivenessState::from_liveness(&outcome.liveness),
860            idempotent: outcome.idempotent,
861            failure: outcome
862                .failure
863                .as_ref()
864                .map(DashboardChildControlFailure::from_failure),
865            generation_fence: outcome
866                .generation_fence
867                .as_ref()
868                .map(DashboardGenerationFenceOutcome::from),
869        }
870    }
871}
872
873/// Command result shape returned through dashboard state deltas.
874#[allow(clippy::large_enum_variant)]
875#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
876#[serde(tag = "type", rename_all = "snake_case")]
877pub enum DashboardCommandResult {
878    /// Child was accepted by the control loop.
879    ChildAdded {
880        /// Child manifest stored by the runtime.
881        child_manifest: String,
882    },
883    /// Child control result after a command.
884    ChildControl {
885        /// Dashboard child control result.
886        outcome: DashboardChildControlResult,
887    },
888    /// Current state query result.
889    CurrentState {
890        /// Runtime current state.
891        state: DashboardCurrentState,
892    },
893    /// Shutdown command result.
894    Shutdown {
895        /// Shutdown result serialized by the shutdown module.
896        result: Value,
897    },
898}
899
900impl DashboardCommandResult {
901    /// Converts a runtime command result into a dashboard command result.
902    ///
903    /// # Arguments
904    ///
905    /// - `result`: Runtime command result.
906    ///
907    /// # Returns
908    ///
909    /// Returns a dashboard command result.
910    pub fn from_command_result(result: &CommandResult) -> Result<Self, serde_json::Error> {
911        match result {
912            CommandResult::ChildAdded { child_manifest } => Ok(Self::ChildAdded {
913                child_manifest: child_manifest.clone(),
914            }),
915            CommandResult::ChildControl { outcome } => Ok(Self::ChildControl {
916                outcome: DashboardChildControlResult::from_child_control_result(outcome),
917            }),
918            CommandResult::CurrentState { state } => Ok(Self::CurrentState {
919                state: DashboardCurrentState::from_current_state(state),
920            }),
921            CommandResult::Shutdown { result } => Ok(Self::Shutdown {
922                result: serde_json::to_value(result)?,
923            }),
924        }
925    }
926}
927
928/// Serializes a runtime command result using the dashboard return model.
929///
930/// # Arguments
931///
932/// - `result`: Runtime command result.
933///
934/// # Returns
935///
936/// Returns a JSON value with the dashboard command result shape.
937pub fn dashboard_command_result_value(result: &CommandResult) -> Result<Value, serde_json::Error> {
938    serde_json::to_value(DashboardCommandResult::from_command_result(result)?)
939}
940
941/// Derives the managed child state that corresponds to an operation.
942///
943/// # Arguments
944///
945/// - `operation`: Runtime control operation.
946///
947/// Converts a child runtime record into the existing dashboard runtime row.
948///
949/// # Arguments
950///
951/// - `record`: Runtime child record returned by current state.
952/// - `shutdown_completed`: Whether the supervisor shutdown has completed.
953///
954/// # Returns
955///
956/// Returns a dashboard runtime row that preserves the existing UI list shape.
957pub fn runtime_state_from_child_runtime_record(
958    record: &ChildRuntimeRecord,
959    shutdown_completed: bool,
960) -> RuntimeState {
961    let managed_child_state = DashboardManagedChildState::from(record.operation);
962    let readiness = DashboardReadinessState::from(record.liveness.readiness);
963    RuntimeState {
964        child_path: record.path.to_string(),
965        lifecycle_state: managed_child_state.as_label().to_owned(),
966        health: if record.liveness.heartbeat_stale {
967            "stale".to_owned()
968        } else {
969            "healthy".to_owned()
970        },
971        readiness: readiness.as_label().to_owned(),
972        generation: record
973            .generation
974            .map(|generation| generation.value)
975            .unwrap_or(0),
976        child_start_count: record.attempt.map(|attempt| attempt.value).unwrap_or(0),
977        restart_count: u64::from(record.restart_limit.used),
978        last_failure: record
979            .failure
980            .as_ref()
981            .map(|failure| failure.reason.clone()),
982        last_policy_decision: Some(format!(
983            "restart_limit_remaining={}",
984            record.restart_limit.remaining
985        )),
986        shutdown_state: if shutdown_completed {
987            "completed".to_owned()
988        } else {
989            "running".to_owned()
990        },
991    }
992}
993
994/// Event record streamed from a target process.
995#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
996pub struct EventRecord {
997    /// Target process identifier.
998    pub target_id: String,
999    /// Target-local monotonic sequence.
1000    pub sequence: u64,
1001    /// Correlation identifier.
1002    pub correlation_id: String,
1003    /// Event type label.
1004    pub event_type: String,
1005    /// Severity label.
1006    pub severity: String,
1007    /// Target path.
1008    pub target_path: String,
1009    /// Optional child identifier.
1010    pub child_id: Option<String>,
1011    /// Occurred time as Unix nanoseconds (string in JSON).
1012    #[serde(serialize_with = "serialize_nanos")]
1013    pub occurred_at_unix_nanos: u128,
1014    /// Configuration version.
1015    pub config_version: String,
1016    /// Event payload.
1017    pub payload: Value,
1018}
1019
1020/// Log record streamed from a target process.
1021#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1022pub struct LogRecord {
1023    /// Target process identifier.
1024    pub target_id: String,
1025    /// Optional target-local sequence.
1026    pub sequence: Option<u64>,
1027    /// Optional correlation identifier.
1028    pub correlation_id: Option<String>,
1029    /// Severity label.
1030    pub severity: String,
1031    /// Log message.
1032    pub message: String,
1033    /// Structured log fields.
1034    pub fields: BTreeMap<String, String>,
1035    /// Occurred time as Unix nanoseconds (string in JSON).
1036    #[serde(serialize_with = "serialize_nanos")]
1037    pub occurred_at_unix_nanos: u128,
1038}
1039
1040/// Supported control command names.
1041#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1042#[serde(rename_all = "snake_case")]
1043pub enum ControlCommandKind {
1044    /// Restart a child.
1045    RestartChild,
1046    /// Pause a child.
1047    PauseChild,
1048    /// Resume a child.
1049    ResumeChild,
1050    /// Quarantine a child.
1051    QuarantineChild,
1052    /// Remove a child.
1053    RemoveChild,
1054    /// Add a child.
1055    AddChild,
1056    /// Shut down the whole tree.
1057    ShutdownTree,
1058}
1059
1060/// Target selector for a control command.
1061#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1062pub struct ControlCommandTarget {
1063    /// Optional child path for child-scoped commands.
1064    pub child_path: Option<String>,
1065    /// Optional child manifest for add-child commands.
1066    pub child_manifest: Option<String>,
1067}
1068
1069/// Control command request forwarded by relay to target IPC.
1070#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1071pub struct ControlCommandRequest {
1072    /// Command identifier.
1073    pub command_id: String,
1074    /// Target process identifier.
1075    pub target_id: String,
1076    /// Command kind.
1077    pub command: ControlCommandKind,
1078    /// Command target.
1079    pub target: ControlCommandTarget,
1080    /// Non-empty reason.
1081    pub reason: String,
1082    /// Authenticated requester derived by relay.
1083    pub requested_by: String,
1084    /// Whether dangerous command confirmation is present.
1085    pub confirmed: bool,
1086    #[serde(
1087        serialize_with = "serialize_nanos",
1088        deserialize_with = "deserialize_nanos"
1089    )]
1090    /// Request time as Unix nanoseconds.
1091    pub requested_at_unix_nanos: u128,
1092}
1093
1094/// Control command result returned by target IPC.
1095#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1096pub struct ControlCommandResult {
1097    /// Command identifier.
1098    pub command_id: String,
1099    /// Target process identifier.
1100    pub target_id: String,
1101    /// Whether target accepted the command.
1102    pub accepted: bool,
1103    /// Status label.
1104    pub status: String,
1105    /// Optional structured error.
1106    pub error: Option<crate::dashboard::error::DashboardError>,
1107    /// Optional state delta.
1108    pub state_delta: Option<Value>,
1109    /// Completion time as Unix nanoseconds.
1110    #[serde(
1111        default,
1112        skip_serializing_if = "Option::is_none",
1113        serialize_with = "serialize_nanos_opt"
1114    )]
1115    /// Completion time as Unix nanoseconds.
1116    pub completed_at_unix_nanos: Option<u128>,
1117}
1118
1119/// Audit event emitted for accepted, rejected, and completed commands.
1120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1121pub struct AuditEvent {
1122    /// Audit event identifier.
1123    pub audit_id: String,
1124    /// Remote identity summary.
1125    pub identity: String,
1126    /// Target process identifier.
1127    pub target_id: String,
1128    /// Command identifier.
1129    pub command_id: String,
1130    /// Command kind.
1131    pub command: ControlCommandKind,
1132    /// Command target.
1133    pub target: ControlCommandTarget,
1134    /// Operator-provided reason.
1135    pub reason: String,
1136    /// Result summary.
1137    pub result: String,
1138    /// Occurred time as Unix nanoseconds (string in JSON).
1139    #[serde(serialize_with = "serialize_nanos")]
1140    pub occurred_at_unix_nanos: u128,
1141}