ic_query/ic/node_status/model.rs
1//! Module: ic::node_status::model
2//!
3//! Responsibility: public observed node-status requests, evidence, reports, and errors.
4//! Does not own: source calls, cache IO, projection, or rendering.
5//! Boundary: preserves raw Dashboard status data and explicit off-chain/cache provenance.
6
7use crate::ic::IcDashboardReportProvenance;
8use serde::{Deserialize as SerdeDeserialize, Serialize};
9use std::fmt;
10#[cfg(feature = "dashboard-host")]
11use std::path::PathBuf;
12use thiserror::Error as ThisError;
13
14///
15/// IcNodeStatusScope
16///
17/// Dashboard collection scope retained by an observed node-status snapshot.
18///
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum IcNodeStatusScope {
23 /// Dashboard's default public-mainnet node set, excluding cloud-engine Type4 nodes.
24 DashboardMainnetDefault,
25}
26
27impl IcNodeStatusScope {
28 /// Return the stable serialized scope label.
29 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::DashboardMainnetDefault => super::IC_NODE_STATUS_SCOPE,
33 }
34 }
35}
36
37///
38/// IcNodeOperationalStatus
39///
40/// Known Dashboard node-status classification used for filtering and counts.
41///
42
43#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
44#[serde(rename_all = "snake_case")]
45pub enum IcNodeOperationalStatus {
46 /// The Dashboard reports the node as up.
47 Up,
48 /// The Dashboard reports the node as down.
49 Down,
50 /// The Dashboard reports the node as administratively disabled.
51 Disabled,
52 /// The Dashboard reports degraded operation.
53 Degraded,
54 /// The raw Dashboard value is not recognized by this release.
55 Unknown,
56}
57
58impl IcNodeOperationalStatus {
59 /// Classify raw Dashboard status text without discarding unknown future values.
60 #[must_use]
61 pub fn from_raw(raw: &str) -> Self {
62 match raw {
63 "UP" => Self::Up,
64 "DOWN" => Self::Down,
65 "DISABLED" => Self::Disabled,
66 "DEGRADED" => Self::Degraded,
67 _ => Self::Unknown,
68 }
69 }
70
71 /// Return the stable lowercase display label.
72 #[must_use]
73 pub const fn as_str(self) -> &'static str {
74 match self {
75 Self::Up => "up",
76 Self::Down => "down",
77 Self::Disabled => "disabled",
78 Self::Degraded => "degraded",
79 Self::Unknown => "unknown",
80 }
81 }
82}
83
84impl fmt::Display for IcNodeOperationalStatus {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 formatter.write_str(self.as_str())
87 }
88}
89
90///
91/// IcNodeStatusCounts
92///
93/// Exact status totals derived from raw observed node rows.
94///
95
96#[derive(Clone, Debug, Default, Eq, PartialEq, SerdeDeserialize, Serialize)]
97pub struct IcNodeStatusCounts {
98 /// Total observed rows in the group.
99 pub total: usize,
100 /// Rows classified as `UP`.
101 pub up: usize,
102 /// Rows classified as `DOWN`.
103 pub down: usize,
104 /// Rows classified as `DISABLED`.
105 pub disabled: usize,
106 /// Rows classified as `DEGRADED`.
107 pub degraded: usize,
108 /// Rows whose raw status is not recognized.
109 pub unknown: usize,
110}
111
112impl IcNodeStatusCounts {
113 /// Return every row not classified as raw Dashboard `UP`.
114 #[must_use]
115 pub const fn non_up(&self) -> usize {
116 self.total.saturating_sub(self.up)
117 }
118}
119
120///
121/// IcNodeAssignmentStatusCounts
122///
123/// Operational-status totals partitioned by mutually exclusive assignment class.
124///
125
126#[derive(Clone, Debug, Default, Eq, PartialEq, SerdeDeserialize, Serialize)]
127pub struct IcNodeAssignmentStatusCounts {
128 /// Status totals for nodes with an observed assigned Subnet.
129 pub assigned: IcNodeStatusCounts,
130 /// Status totals for rows whose raw node type is `UNASSIGNED`.
131 pub unassigned: IcNodeStatusCounts,
132 /// Status totals for rows whose raw node type is `API_BOUNDARY`.
133 pub api_boundary: IcNodeStatusCounts,
134 /// Status totals for rows without a known assignment class.
135 pub unknown: IcNodeStatusCounts,
136}
137
138///
139/// IcNodeStatusGroupCounts
140///
141/// Overall and assignment-partitioned status totals for one aggregate group.
142///
143
144#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
145pub struct IcNodeStatusGroupCounts {
146 /// Raw operational-status totals for the group.
147 pub statuses: IcNodeStatusCounts,
148 /// Raw operational-status totals partitioned by assignment class.
149 pub assignment_statuses: IcNodeAssignmentStatusCounts,
150}
151
152///
153/// IcNodeCountComparison
154///
155/// Ordering of one observed node count relative to another.
156///
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
159#[serde(rename_all = "snake_case")]
160pub enum IcNodeCountComparison {
161 /// The left-hand count is smaller.
162 Less,
163 /// The two counts are equal.
164 Equal,
165 /// The left-hand count is larger.
166 Greater,
167}
168
169impl IcNodeCountComparison {
170 /// Compare a left-hand count with a right-hand count.
171 #[must_use]
172 pub const fn from_counts(left: usize, right: usize) -> Self {
173 if left < right {
174 Self::Less
175 } else if left > right {
176 Self::Greater
177 } else {
178 Self::Equal
179 }
180 }
181
182 /// Return the stable lowercase label.
183 #[must_use]
184 pub const fn as_str(self) -> &'static str {
185 match self {
186 Self::Less => "less",
187 Self::Equal => "equal",
188 Self::Greater => "greater",
189 }
190 }
191}
192
193///
194/// IcNodeCountComparisonCounts
195///
196/// Number of provider groups in each node-count comparison outcome.
197///
198
199#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
200pub struct IcNodeCountComparisonCounts {
201 /// Provider groups whose left-hand count is smaller.
202 pub less: usize,
203 /// Provider groups whose counts are equal.
204 pub equal: usize,
205 /// Provider groups whose left-hand count is larger.
206 pub greater: usize,
207}
208
209///
210/// IcNodeStatusRow
211///
212/// One raw node observation retained from the official Dashboard node resource.
213///
214
215#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
216pub struct IcNodeStatusRow {
217 /// Canonical node principal.
218 pub node_id: String,
219 /// Canonical node-operator principal.
220 pub node_operator_id: String,
221 /// Canonical node-provider principal.
222 pub node_provider_id: String,
223 /// Off-chain Dashboard node-provider name.
224 pub node_provider_name: String,
225 /// Raw Dashboard node type.
226 pub node_type: String,
227 /// Raw Registry reward type reported by the Dashboard.
228 pub node_reward_type: String,
229 /// Raw Dashboard operational status.
230 pub status: String,
231 /// Raw Dashboard alert name when present.
232 pub alert_name: Option<String>,
233 /// Assigned Subnet principal when present.
234 pub subnet_id: Option<String>,
235 /// Cloud-engine Subnet principal when present in a future compatible scope.
236 pub cloud_engine_subnet_id: Option<String>,
237 /// Dashboard data-center identifier.
238 pub data_center_id: String,
239 /// Dashboard data-center name.
240 pub data_center_name: String,
241 /// Dashboard infrastructure owner label.
242 pub owner: String,
243 /// Raw Dashboard geographic region label.
244 pub region: String,
245 /// Observed GuestOS version when reported.
246 pub guestos_version: Option<String>,
247 /// Observed GuestOS trusted-execution status when reported.
248 pub guestos_tee_active: Option<bool>,
249 /// Observed node IP address when reported.
250 pub ip_address: Option<String>,
251 /// Observed IPv4-connectivity state when reported.
252 pub ipv4_connectivity_status: Option<bool>,
253 /// Dashboard hardware-generation label when reported.
254 pub node_hardware_generation: Option<String>,
255}
256
257impl IcNodeStatusRow {
258 /// Return the known classification of this row's raw status.
259 #[must_use]
260 pub fn operational_status(&self) -> IcNodeOperationalStatus {
261 IcNodeOperationalStatus::from_raw(&self.status)
262 }
263
264 /// Return whether the raw row is anything other than known `UP`.
265 #[must_use]
266 pub fn is_non_up(&self) -> bool {
267 self.operational_status() != IcNodeOperationalStatus::Up
268 }
269}
270
271///
272/// IcNodeStatusCacheEvidence
273///
274/// Caller-relative local cache evidence attached to a projected status report.
275///
276
277#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
278pub struct IcNodeStatusCacheEvidence {
279 /// Canonical local cache path used by the report.
280 pub cache_path: String,
281 /// Whether the cache is within the default age policy for this caller's time.
282 pub cache_fresh: bool,
283 /// Cache age at report construction.
284 pub age_seconds: u64,
285 /// Age threshold used to classify freshness.
286 pub stale_after_seconds: u64,
287}
288
289///
290/// IcNodeStatusObservation
291///
292/// Shared source, scope, and optional cache evidence for status reports.
293///
294
295#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
296pub struct IcNodeStatusObservation {
297 #[serde(flatten)]
298 /// Official Dashboard source provenance.
299 pub source: IcDashboardReportProvenance,
300 /// Explicit node collection scope.
301 pub scope: IcNodeStatusScope,
302 /// Whether cloud-engine nodes were included in the source collection.
303 pub cloud_engine_nodes_included: bool,
304 /// Local cache evidence, absent for a pure live snapshot.
305 pub cache: Option<IcNodeStatusCacheEvidence>,
306}
307
308///
309/// IcNodeStatusSnapshot
310///
311/// Complete canonical observed node snapshot used by every status projection.
312///
313
314#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
315pub struct IcNodeStatusSnapshot {
316 #[serde(flatten)]
317 /// Shared source and scope evidence.
318 pub observation: IcNodeStatusObservation,
319 /// Exact number of raw node rows.
320 pub node_count: usize,
321 /// Snapshot-wide overall and assignment-partitioned totals.
322 pub counts: IcNodeStatusGroupCounts,
323 /// Canonically ordered raw node rows.
324 pub nodes: Vec<IcNodeStatusRow>,
325}
326
327///
328/// IcNodeStatusSnapshotRequest
329///
330/// Request for one live finite Dashboard node-status snapshot.
331///
332
333#[derive(Clone, Debug, Eq, PartialEq)]
334pub struct IcNodeStatusSnapshotRequest {
335 /// Dashboard API v3 base endpoint.
336 pub source_endpoint: String,
337 /// Caller-supplied collection timestamp as Unix seconds.
338 pub now_unix_secs: u64,
339}
340
341impl IcNodeStatusSnapshotRequest {
342 /// Construct one live node-status snapshot request.
343 #[must_use]
344 pub fn new(source_endpoint: impl Into<String>, now_unix_secs: u64) -> Self {
345 Self {
346 source_endpoint: source_endpoint.into(),
347 now_unix_secs,
348 }
349 }
350}
351
352///
353/// IcNodeStatusSourceData
354///
355/// Untrusted raw node rows and provenance returned by a Dashboard source capability.
356///
357
358#[cfg(feature = "dashboard-host")]
359#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct IcNodeStatusSourceData {
361 /// Source-call provenance echoed by the source.
362 pub source: crate::ic::IcSourceRequest,
363 /// Explicit Dashboard collection scope.
364 pub scope: IcNodeStatusScope,
365 /// Whether the source claims cloud-engine node inclusion.
366 pub cloud_engine_nodes_included: bool,
367 /// Raw observed node rows.
368 pub nodes: Vec<IcNodeStatusRow>,
369}
370
371///
372/// IcNodeStatusView
373///
374/// Target and attention selection shared by node, Subnet, and provider views.
375///
376
377#[derive(Clone, Debug, Default, Eq, PartialEq)]
378pub struct IcNodeStatusView {
379 /// Optional exact identifier or unique principal prefix.
380 pub target: Option<String>,
381 /// Whether fully-up rows or groups are included without a target.
382 pub include_all: bool,
383}
384
385impl IcNodeStatusView {
386 /// Construct the default attention-only view.
387 #[must_use]
388 pub const fn attention() -> Self {
389 Self {
390 target: None,
391 include_all: false,
392 }
393 }
394
395 /// Select one exact identifier or unique principal prefix.
396 #[must_use]
397 pub fn with_target(mut self, target: impl Into<String>) -> Self {
398 self.target = Some(target.into());
399 self
400 }
401
402 /// Include fully-up rows as well as attention rows.
403 #[must_use]
404 pub const fn with_all(mut self, include_all: bool) -> Self {
405 self.include_all = include_all;
406 self
407 }
408}
409
410///
411/// IcNodeStatusReport
412///
413/// Node-level view over one complete observed status snapshot.
414///
415
416#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
417pub struct IcNodeStatusReport {
418 #[serde(flatten)]
419 /// Shared source, scope, and cache evidence.
420 pub observation: IcNodeStatusObservation,
421 /// Total rows in the complete source snapshot.
422 pub snapshot_node_count: usize,
423 /// Snapshot-wide overall and assignment-partitioned totals.
424 pub counts: IcNodeStatusGroupCounts,
425 /// Whether the view includes fully-up rows.
426 pub include_all: bool,
427 /// Target text supplied by the caller.
428 pub requested_target: Option<String>,
429 /// Canonical target resolved by exact or unique-prefix matching.
430 pub resolved_target: Option<String>,
431 /// Stable label describing how the target resolved.
432 pub resolved_from: Option<String>,
433 /// Number of returned node rows.
434 pub returned_node_count: usize,
435 /// Selected canonical node rows.
436 pub nodes: Vec<IcNodeStatusRow>,
437}
438
439///
440/// IcSubnetStatusRow
441///
442/// Observed operational counts and conservative threshold evidence for one Subnet.
443///
444
445#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
446pub struct IcSubnetStatusRow {
447 /// Canonical Subnet principal.
448 pub subnet_id: String,
449 /// Operational-status totals for observed assigned nodes.
450 pub statuses: IcNodeStatusCounts,
451 /// Byzantine fault count `floor((n - 1) / 3)` for the observed membership size.
452 pub fault_tolerance_node_count: usize,
453 /// Additional down nodes required for the down count to exceed the derived threshold.
454 pub additional_down_nodes_to_exceed_fault_tolerance: usize,
455 /// Additional non-up nodes required for the conservative count to exceed the threshold.
456 pub additional_non_up_nodes_to_exceed_fault_tolerance: usize,
457 /// Whether the observed down count already exceeds the derived threshold.
458 pub down_fault_tolerance_exceeded: bool,
459 /// Whether the observed conservative non-up count already exceeds the threshold.
460 pub conservative_non_up_fault_tolerance_exceeded: bool,
461 /// Raw non-up node evidence in canonical node order.
462 pub non_up_nodes: Vec<IcNodeStatusRow>,
463}
464
465///
466/// IcSubnetStatusReport
467///
468/// Subnet-level operational view over one complete observed node snapshot.
469///
470
471#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
472pub struct IcSubnetStatusReport {
473 #[serde(flatten)]
474 /// Shared source, scope, and cache evidence.
475 pub observation: IcNodeStatusObservation,
476 /// Total rows in the complete source snapshot.
477 pub snapshot_node_count: usize,
478 /// Snapshot rows with an assigned Subnet.
479 pub assigned_node_count: usize,
480 /// Number of observed Subnets before view filtering.
481 pub subnet_count: usize,
482 /// Number of observed Subnets containing a non-up node.
483 pub attention_subnet_count: usize,
484 /// Whether fully-up Subnets are included without a target.
485 pub include_all: bool,
486 /// Target text supplied by the caller.
487 pub requested_target: Option<String>,
488 /// Canonical Subnet target resolved by exact or unique-prefix matching.
489 pub resolved_target: Option<String>,
490 /// Stable label describing how the target resolved.
491 pub resolved_from: Option<String>,
492 /// Number of returned Subnet rows.
493 pub returned_subnet_count: usize,
494 /// Canonically ordered selected Subnet rows.
495 pub subnets: Vec<IcSubnetStatusRow>,
496}
497
498///
499/// IcNodeProviderStatusRow
500///
501/// Assignment and raw operational counts for one observed node provider.
502///
503
504#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
505pub struct IcNodeProviderStatusRow {
506 /// Canonical node-provider principal.
507 pub node_provider_id: String,
508 /// Off-chain Dashboard provider name.
509 pub node_provider_name: String,
510 /// Operational-status and assignment totals for the provider.
511 pub counts: IcNodeStatusGroupCounts,
512 /// Unassigned up-node count compared with assigned up-node count.
513 pub unassigned_up_vs_assigned_up: IcNodeCountComparison,
514 /// Unassigned non-up-node count compared with assigned non-up-node count.
515 pub unassigned_non_up_vs_assigned_non_up: IcNodeCountComparison,
516 /// Raw non-up node evidence in canonical node order.
517 pub non_up_nodes: Vec<IcNodeStatusRow>,
518}
519
520///
521/// IcNodeProviderStatusReport
522///
523/// Node-provider operational view over one complete observed node snapshot.
524///
525
526#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
527pub struct IcNodeProviderStatusReport {
528 #[serde(flatten)]
529 /// Shared source, scope, and cache evidence.
530 pub observation: IcNodeStatusObservation,
531 /// Total rows in the complete source snapshot.
532 pub snapshot_node_count: usize,
533 /// Number of observed provider principals before view filtering.
534 pub provider_count: usize,
535 /// Number of provider groups containing a non-up node.
536 pub attention_provider_count: usize,
537 /// All-provider outcomes for unassigned up nodes compared with assigned up nodes.
538 pub unassigned_up_vs_assigned_up_provider_counts: IcNodeCountComparisonCounts,
539 /// All-provider outcomes for unassigned non-up nodes compared with assigned non-up nodes.
540 pub unassigned_non_up_vs_assigned_non_up_provider_counts: IcNodeCountComparisonCounts,
541 /// Whether fully-up providers are included without a target.
542 pub include_all: bool,
543 /// Target text supplied by the caller.
544 pub requested_target: Option<String>,
545 /// Canonical provider target resolved by exact or unique-prefix matching.
546 pub resolved_target: Option<String>,
547 /// Stable label describing how the target resolved.
548 pub resolved_from: Option<String>,
549 /// Number of returned provider rows.
550 pub returned_provider_count: usize,
551 /// Canonically ordered selected provider rows.
552 pub providers: Vec<IcNodeProviderStatusRow>,
553}
554
555///
556/// IcNodeStatusProjectionError
557///
558/// Failure to resolve or project a requested observed status view.
559///
560
561#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
562pub enum IcNodeStatusProjectionError {
563 /// Snapshot-level counts or canonical ordering do not match raw rows.
564 #[error("invalid observed node-status snapshot: {reason}")]
565 InvalidSnapshot {
566 /// Deterministic validation failure.
567 reason: String,
568 },
569 /// The supplied target is empty after trimming.
570 #[error("{kind} target must not be empty")]
571 EmptyTarget {
572 /// Projection kind being selected.
573 kind: &'static str,
574 },
575 /// No observed identifier matches the supplied target.
576 #[error("{kind} target {target:?} did not match the observed snapshot")]
577 UnknownTarget {
578 /// Projection kind being selected.
579 kind: &'static str,
580 /// Unmatched target text.
581 target: String,
582 },
583 /// More than one observed identifier matches the supplied prefix.
584 #[error("{kind} prefix {prefix:?} is ambiguous; matches: {matches:?}")]
585 AmbiguousTarget {
586 /// Projection kind being selected.
587 kind: &'static str,
588 /// Ambiguous principal prefix.
589 prefix: String,
590 /// Canonically ordered matching principals.
591 matches: Vec<String>,
592 },
593}
594
595///
596/// IcNodeStatusCacheRequest
597///
598/// Stable identity of one network-level observed node-status cache.
599///
600
601#[cfg(feature = "dashboard-host")]
602#[derive(Clone, Debug, Eq, PartialEq)]
603pub struct IcNodeStatusCacheRequest {
604 /// Root directory containing all `ic-query` caches.
605 pub cache_root: PathBuf,
606 /// Requested network name.
607 pub network: String,
608}
609
610#[cfg(feature = "dashboard-host")]
611impl IcNodeStatusCacheRequest {
612 /// Construct one observed node-status cache identity.
613 #[must_use]
614 pub fn new(cache_root: impl Into<PathBuf>, network: impl Into<String>) -> Self {
615 Self {
616 cache_root: cache_root.into(),
617 network: network.into(),
618 }
619 }
620}
621
622///
623/// IcNodeStatusRefreshRequest
624///
625/// Settings for one forced observed node-status snapshot refresh.
626///
627
628#[cfg(feature = "dashboard-host")]
629#[derive(Clone, Debug, Eq, PartialEq)]
630pub struct IcNodeStatusRefreshRequest {
631 /// Stable cache identity.
632 pub cache: IcNodeStatusCacheRequest,
633 /// Explicit Dashboard API base endpoint.
634 pub source_endpoint: String,
635 /// Caller-supplied observation time as Unix seconds.
636 pub now_unix_secs: u64,
637 /// Age after which an abandoned refresh lock can be reclaimed.
638 pub lock_stale_after_seconds: u64,
639}
640
641#[cfg(feature = "dashboard-host")]
642impl IcNodeStatusRefreshRequest {
643 /// Construct one forced node-status snapshot refresh request.
644 #[must_use]
645 pub fn new(
646 cache_root: impl Into<PathBuf>,
647 network: impl Into<String>,
648 source_endpoint: impl Into<String>,
649 now_unix_secs: u64,
650 lock_stale_after_seconds: u64,
651 ) -> Self {
652 Self {
653 cache: IcNodeStatusCacheRequest::new(cache_root, network),
654 source_endpoint: source_endpoint.into(),
655 now_unix_secs,
656 lock_stale_after_seconds,
657 }
658 }
659}
660
661///
662/// IcNodeStatusReadRequest
663///
664/// Cache-backed status-view request shared by node, Subnet, and provider reports.
665///
666
667#[cfg(feature = "dashboard-host")]
668#[derive(Clone, Debug, Eq, PartialEq)]
669pub struct IcNodeStatusReadRequest {
670 /// Live and cache settings used when refresh is required.
671 pub refresh: IcNodeStatusRefreshRequest,
672 /// Projection target and attention selection.
673 pub view: IcNodeStatusView,
674 /// Whether to force a live replacement before reading.
675 pub force_refresh: bool,
676}
677
678#[cfg(feature = "dashboard-host")]
679impl IcNodeStatusReadRequest {
680 /// Construct one stale-refresh status-view request.
681 #[must_use]
682 pub fn new(
683 cache_root: impl Into<PathBuf>,
684 network: impl Into<String>,
685 source_endpoint: impl Into<String>,
686 now_unix_secs: u64,
687 ) -> Self {
688 Self {
689 refresh: IcNodeStatusRefreshRequest::new(
690 cache_root,
691 network,
692 source_endpoint,
693 now_unix_secs,
694 super::DEFAULT_IC_NODE_STATUS_REFRESH_LOCK_STALE_SECONDS,
695 ),
696 view: IcNodeStatusView::attention(),
697 force_refresh: false,
698 }
699 }
700
701 /// Apply one target and attention selection.
702 #[must_use]
703 pub fn with_view(mut self, view: IcNodeStatusView) -> Self {
704 self.view = view;
705 self
706 }
707
708 /// Select a forced refresh before projecting the report.
709 #[must_use]
710 pub const fn with_force_refresh(mut self, force_refresh: bool) -> Self {
711 self.force_refresh = force_refresh;
712 self
713 }
714}
715
716///
717/// IcNodeStatusRefreshReport
718///
719/// Result of atomically replacing the complete observed node-status cache.
720///
721
722#[cfg(feature = "dashboard-host")]
723#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
724pub struct IcNodeStatusRefreshReport {
725 /// Cache/report schema version.
726 pub schema_version: u32,
727 /// Network identity attached to the replacement.
728 pub network: String,
729 /// Explicit Dashboard endpoint queried.
730 pub source_endpoint: String,
731 /// Canonical UTC collection timestamp.
732 pub fetched_at: String,
733 /// Collector identity.
734 pub fetched_by: String,
735 /// Published cache path.
736 pub cache_path: String,
737 /// Sibling refresh-lock path.
738 pub refresh_lock_path: String,
739 /// Whether an existing complete snapshot was replaced.
740 pub replaced_existing_cache: bool,
741 /// Number of published node rows.
742 pub node_count: usize,
743 /// Published overall and assignment-partitioned status totals.
744 pub counts: IcNodeStatusGroupCounts,
745}
746
747///
748/// IcNodeStatusHostError
749///
750/// Host source, cache, and projection failures for observed node-status reports.
751///
752
753#[cfg(feature = "dashboard-host")]
754#[derive(Debug, ThisError)]
755pub enum IcNodeStatusHostError {
756 /// The observed Dashboard source supports only public mainnet identity.
757 #[error("observed IC node status supports only the mainnet `ic` network, not {network:?}")]
758 UnsupportedNetwork {
759 /// Unsupported requested network.
760 network: String,
761 },
762 /// Live-source or source-validation failure.
763 #[error(transparent)]
764 Source(#[from] crate::ic::IcHostError),
765 /// Pure projection failure.
766 #[error(transparent)]
767 Projection(#[from] IcNodeStatusProjectionError),
768 /// Strict cache load found no complete snapshot.
769 #[error("observed node-status cache is missing at {}", path.display())]
770 MissingCache {
771 /// Expected cache path.
772 path: PathBuf,
773 },
774 /// Existing cache snapshot-key identity does not match its path.
775 #[error(
776 "observed node-status cache identity mismatch at {}: {field} is {actual:?}, expected {expected:?}",
777 path.display()
778 )]
779 CacheIdentityMismatch {
780 /// Cache path that failed validation.
781 path: PathBuf,
782 /// Identity field that differs.
783 field: &'static str,
784 /// Required identity value.
785 expected: String,
786 /// Stored identity value.
787 actual: String,
788 },
789 /// Existing cache is structurally readable but semantically invalid.
790 #[error("invalid observed node-status cache at {}: {reason}", path.display())]
791 InvalidCache {
792 /// Cache path that failed validation.
793 path: PathBuf,
794 /// Deterministic validation failure.
795 reason: String,
796 },
797 /// Shared generic JSON, filesystem, or refresh-lock cache failure.
798 #[error(transparent)]
799 Cache(#[from] crate::cache_file::HostCacheError),
800}