zenkey_fleet/report/node.rs
1//! The node plane (RFC 04 §5): who is up, what they run, and which
2//! deployment bases were seen at all.
3
4use serde::Serialize;
5
6/// One producer on one origin — row-shaped so a `--watch` loop can diff it
7/// and a GUI can select it (`origin/producer` is the stable row identity).
8#[derive(Debug, Clone, Serialize)]
9pub struct NodeRow {
10 pub origin: String,
11 /// Live producer name (from the liveliness token; zero payload).
12 pub producer: String,
13 /// The producing app, when a registry slice joined (`--verbose`).
14 /// `None` = not asked / no slice — never a default (O4).
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub app: Option<String>,
17 /// Registry version from the joined slice, same provenance rule.
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub registry_version: Option<String>,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct NodeList {
24 pub nodes: Vec<NodeRow>,
25 /// Whether a slice join was even attempted (`--verbose`) — keeps "asked,
26 /// no slice served" distinguishable from "not asked" in rows whose
27 /// `app`/`registry_version` are `None` (O4).
28 pub slices_joined: bool,
29}
30
31#[derive(Debug, Clone, Serialize)]
32pub struct BaseList {
33 /// Discovered bases; `base` is a plain string, `""` for the empty base.
34 pub bases: Vec<crate::DiscoveredBase>,
35}
36
37/// One producer's story on one node — the enrichment §6.3 promised
38/// (issue #40). Every field is honest about its provenance: absent
39/// introspection is `None`, never a default (RFC 09 §5.1 O4).
40#[derive(Debug, Clone, serde::Serialize)]
41pub struct ProducerInfo {
42 pub name: String,
43 /// A liveliness token stands (RFC 04 §5 — the only presence signal).
44 pub alive: bool,
45 /// From this origin's served introspect slice, when it answered.
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub app: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub registry_version: Option<String>,
50 pub subjects: usize,
51 pub procedures: usize,
52 #[serde(skip_serializing_if = "Vec::is_empty", default)]
53 pub blob_tiers: Vec<String>,
54 /// Declared `@media` streams (RFC 08 §2/§6, v1.16 — the slice finally
55 /// carries what §6 claimed through v1.7): discoverable off the bus, so
56 /// a viewer can enumerate streams without a compiled-in registry.
57 #[serde(skip_serializing_if = "Vec::is_empty", default)]
58 pub media: Vec<MediaStreamInfo>,
59 /// Deprecated subjects this build still serves — RFC 08 §6's headline
60 /// buy ("which hosts still serve a deprecated subject").
61 pub deprecated_served: usize,
62}
63
64/// One declared media stream, as the slice states it (RFC 08 §2).
65#[derive(Debug, Clone, serde::Serialize)]
66pub struct MediaStreamInfo {
67 /// The stream pattern after `@media/<producer>/`.
68 pub path: String,
69 /// The declared wire encoding (`image/jpeg`, `video/*`).
70 pub encoding: String,
71}
72
73/// Freshness of one declared state subject on this node (RFC 04 §1.2).
74#[derive(Debug, Clone, serde::Serialize)]
75pub struct Freshness {
76 pub producer: String,
77 pub path: String,
78 pub ttl_s: i64,
79 /// Seconds since the newest matching sample's HLC stamp; `None` when no
80 /// sample answered — which is "not seen", not "fresh" (O4).
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub age_s: Option<i64>,
83 /// `age > ttl`, or no sample at all for a declared live subject.
84 pub stale: bool,
85}
86
87/// One node, joined: liveliness × introspect × state freshness.
88#[derive(Debug, Clone, serde::Serialize)]
89pub struct NodeInfo {
90 pub origin: String,
91 pub producers: Vec<ProducerInfo>,
92 #[serde(skip_serializing_if = "Vec::is_empty", default)]
93 pub freshness: Vec<Freshness>,
94}