@@ -9,7 +9,8 @@ use super::get_admin_client;
99use crate :: exit_code:: ExitCode ;
1010use crate :: output:: Formatter ;
1111use rc_core:: admin:: {
12- AdminApi , HealRuntimeState , HealScanMode , HealStartRequest , HealStatus , HealTaskRequest ,
12+ AdminApi , BackgroundHealCoverage , HealRuntimeState , HealScanMode , HealStartRequest , HealStatus ,
13+ HealTaskRequest ,
1314} ;
1415
1516const HEAL_STOP_SUCCESS_MESSAGE : & str = "Heal operation stopped successfully" ;
@@ -104,6 +105,10 @@ struct HealStatusOutput {
104105 #[ serde( skip_serializing_if = "Option::is_none" ) ]
105106 state : Option < HealRuntimeState > ,
106107 #[ serde( skip_serializing_if = "Option::is_none" ) ]
108+ cluster_status_complete : Option < bool > ,
109+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
110+ coverage : Option < BackgroundHealCoverage > ,
111+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
107112 summary : Option < String > ,
108113 #[ serde( skip_serializing_if = "Option::is_none" ) ]
109114 detail : Option < String > ,
@@ -131,6 +136,8 @@ impl From<&HealStatus> for HealStatusOutput {
131136 heal_id : status. heal_id . clone ( ) ,
132137 healing : status. healing ,
133138 state : status. state ,
139+ cluster_status_complete : status. cluster_status_complete ,
140+ coverage : status. coverage . clone ( ) ,
134141 summary : status. summary . clone ( ) ,
135142 detail : status. detail . clone ( ) ,
136143 bucket : status. bucket . clone ( ) ,
@@ -153,6 +160,8 @@ impl From<&HealStatus> for HealStatusOutput {
153160fn has_heal_status_details ( status : & HealStatus ) -> bool {
154161 status. healing
155162 || status. state . is_some ( )
163+ || status. cluster_status_complete . is_some ( )
164+ || status. coverage . is_some ( )
156165 || !status. heal_id . is_empty ( )
157166 || status. summary . is_some ( )
158167 || status. detail . is_some ( )
@@ -210,13 +219,29 @@ enum HealStatusIndicator {
210219 Date ( & ' static str ) ,
211220}
212221
222+ fn cluster_heal_status_is_incomplete ( status : & HealStatus ) -> bool {
223+ status. state == Some ( HealRuntimeState :: Degraded )
224+ || status. cluster_status_complete == Some ( false )
225+ || status. coverage . as_ref ( ) . is_some_and ( |coverage| {
226+ coverage. unknown . is_some_and ( |count| count > 0 )
227+ || !coverage. reasons . is_empty ( )
228+ || matches ! (
229+ ( coverage. responded, coverage. expected) ,
230+ ( Some ( responded) , Some ( expected) ) if responded < expected
231+ )
232+ } )
233+ }
234+
213235fn heal_status_indicator ( status : & HealStatus ) -> HealStatusIndicator {
214236 match status. state {
237+ Some ( HealRuntimeState :: Active ) => HealStatusIndicator :: Progress ( "In Progress" ) ,
238+ Some ( HealRuntimeState :: Degraded ) => HealStatusIndicator :: Date ( "Degraded" ) ,
239+ Some ( HealRuntimeState :: Unknown ) => HealStatusIndicator :: Date ( "Unknown" ) ,
240+ _ if cluster_heal_status_is_incomplete ( status) => HealStatusIndicator :: Date ( "Unknown" ) ,
215241 Some ( HealRuntimeState :: Disabled ) => HealStatusIndicator :: Date ( "Disabled" ) ,
216242 Some ( HealRuntimeState :: Uninitialized ) => HealStatusIndicator :: Date ( "Uninitialized" ) ,
217- Some ( HealRuntimeState :: Active ) => HealStatusIndicator :: Progress ( "In Progress" ) ,
218243 Some ( HealRuntimeState :: Idle ) => HealStatusIndicator :: Date ( "Idle" ) ,
219- Some ( HealRuntimeState :: Unknown ) | None => match status. summary . as_deref ( ) {
244+ None => match status. summary . as_deref ( ) {
220245 Some ( "running" ) => HealStatusIndicator :: Progress ( "In Progress" ) ,
221246 Some ( "finished" ) => HealStatusIndicator :: Progress ( "Finished" ) ,
222247 Some ( "stopped" ) => HealStatusIndicator :: Date ( "Stopped" ) ,
@@ -287,6 +312,42 @@ fn print_heal_status(status: &HealStatus, formatter: &Formatter) {
287312 ) ) ;
288313 formatter. println ( "" ) ;
289314
315+ let incomplete = cluster_heal_status_is_incomplete ( status) ;
316+ let unknown = status. state == Some ( HealRuntimeState :: Unknown ) ;
317+ if incomplete {
318+ formatter
319+ . warning ( "Cluster heal status is incomplete; unavailable peers may still be healing." ) ;
320+ } else if unknown {
321+ formatter. warning ( "Heal runtime state is unknown; cluster idleness cannot be confirmed." ) ;
322+ }
323+
324+ // Token responses describe one task, not the coverage of a cluster snapshot.
325+ if status. summary . is_none ( ) && status. heal_id . is_empty ( ) {
326+ let completeness = if incomplete {
327+ "Incomplete"
328+ } else if status. cluster_status_complete == Some ( true ) {
329+ "Complete"
330+ } else {
331+ "Unknown (not reported by server)"
332+ } ;
333+ formatter. println ( & format ! ( " Cluster status: {completeness}" ) ) ;
334+ }
335+ if let Some ( coverage) = & status. coverage {
336+ let count = |value : Option < u64 > | value. map_or_else ( || "?" . to_string ( ) , |n| n. to_string ( ) ) ;
337+ formatter. println ( & format ! (
338+ " Node coverage: {}/{} responded, {} unknown" ,
339+ count( coverage. responded) ,
340+ count( coverage. expected) ,
341+ count( coverage. unknown) ,
342+ ) ) ;
343+ if !coverage. reasons . is_empty ( ) {
344+ formatter. println ( & format ! (
345+ " Reasons: {}" ,
346+ formatter. sanitize_text( & coverage. reasons. join( ", " ) )
347+ ) ) ;
348+ }
349+ }
350+
290351 if !status. heal_id . is_empty ( ) {
291352 formatter. println ( & format ! ( " Heal ID: {}" , status. heal_id) ) ;
292353 }
@@ -336,6 +397,12 @@ fn print_heal_status(status: &HealStatus, formatter: &Formatter) {
336397 if let Some ( ref last_update) = status. last_update {
337398 formatter. println ( & format ! ( " Last Update: {}" , last_update) ) ;
338399 }
400+ } else if incomplete || unknown {
401+ formatter. println ( " Cluster idleness cannot be confirmed." ) ;
402+ } else if status. state == Some ( HealRuntimeState :: Disabled ) {
403+ formatter. println ( " Heal service is disabled." ) ;
404+ } else if status. state == Some ( HealRuntimeState :: Uninitialized ) {
405+ formatter. println ( " Heal service is not initialized." ) ;
339406 } else {
340407 formatter. println ( " No active heal operation." ) ;
341408 }
@@ -714,4 +781,22 @@ mod tests {
714781 HealStatusIndicator :: Date ( "Disabled" )
715782 ) ) ;
716783 }
784+
785+ #[ test]
786+ fn test_explicit_unknown_overrides_legacy_summary_and_healing ( ) {
787+ for summary in [ None , Some ( "running" ) , Some ( "finished" ) , Some ( "stopped" ) ] {
788+ for healing in [ false , true ] {
789+ let status = HealStatus {
790+ state : Some ( HealRuntimeState :: Unknown ) ,
791+ summary : summary. map ( str:: to_string) ,
792+ healing,
793+ ..Default :: default ( )
794+ } ;
795+ assert ! ( matches!(
796+ heal_status_indicator( & status) ,
797+ HealStatusIndicator :: Date ( "Unknown" )
798+ ) ) ;
799+ }
800+ }
801+ }
717802}
0 commit comments