1use crate::discovery;
4use crate::registry::{PeerInfo, Registry};
5use serde::Serialize;
6use std::collections::HashMap;
7
8#[derive(Debug, Serialize)]
9pub struct GraphData {
10 pub namespace: String,
11 pub local: Vec<Node>,
12 pub peers: Vec<PeerNode>,
13 pub version_mismatches: Vec<String>,
14}
15
16#[derive(Debug, Serialize)]
17pub struct Node {
18 pub name: String,
19 pub version: Option<String>,
20 pub port: u16,
21 pub location: String,
22}
23
24#[derive(Debug, Serialize)]
25pub struct PeerNode {
26 pub id: String,
27 pub addr: String,
28 pub proxy_port: u16,
29 pub services: Vec<Node>,
30}
31
32pub fn collect_graph_data() -> GraphData {
34 collect_graph_data_for(None)
35}
36
37pub fn collect_graph_data_for(namespace: Option<&str>) -> GraphData {
39 let ns = namespace.map(String::from).unwrap_or_else(crate::namespace::current);
40 let registry = Registry::load();
41 let peers = discovery::load_peers(&ns);
42
43 let local: Vec<Node> = registry
44 .local_for(&ns)
45 .map(|(name, e)| Node {
46 name: name.to_string(),
47 version: e.version.clone(),
48 port: e.port,
49 location: "local".to_string(),
50 })
51 .collect();
52
53 let peer_nodes: Vec<PeerNode> = peers
54 .iter()
55 .map(|(id, p)| PeerNode {
56 id: id.clone(),
57 addr: p.addr.clone(),
58 proxy_port: p.proxy_port,
59 services: p
60 .services
61 .iter()
62 .map(|(name, e)| Node {
63 name: name.clone(),
64 version: e.version.clone(),
65 port: e.port,
66 location: format!("peer:{}", p.addr),
67 })
68 .collect(),
69 })
70 .collect();
71
72 let version_mismatches = version_mismatches(®istry, &ns, &peers);
73
74 GraphData {
75 namespace: ns.to_string(),
76 local,
77 peers: peer_nodes,
78 version_mismatches,
79 }
80}
81
82fn version_mismatches(
83 registry: &Registry,
84 namespace: &str,
85 peers: &HashMap<String, PeerInfo>,
86) -> Vec<String> {
87 let mut out = Vec::new();
88 for (name, local_entry) in registry.local_for(namespace) {
89 let local_ver = match &local_entry.version {
90 Some(v) => v,
91 None => continue,
92 };
93 for (_, peer) in peers {
94 if let Some(peer_entry) = peer.services.get(name) {
95 if let Some(peer_ver) = &peer_entry.version {
96 if local_ver != peer_ver {
97 if let (Ok(a), Ok(b)) =
98 (semver::Version::parse(local_ver), semver::Version::parse(peer_ver))
99 {
100 if a.major != b.major {
101 out.push(format!(
102 "{}: local v{} vs peer v{} (major mismatch)",
103 name, local_ver, peer_ver
104 ));
105 } else {
106 out.push(format!(
107 "{}: local v{} vs peer v{}",
108 name, local_ver, peer_ver
109 ));
110 }
111 }
112 }
113 }
114 }
115 }
116 }
117 out
118}
119
120pub fn render_ascii(data: &GraphData) -> String {
121 let mut out = String::new();
122
123 out.push_str(&format!("Namespace: {}\n\n", data.namespace));
124 out.push_str("┌─ Local ──────────────────────┐\n");
125 if data.local.is_empty() {
126 out.push_str("│ (none) │\n");
127 } else {
128 for n in &data.local {
129 let ver = n.version.as_deref().unwrap_or("?");
130 out.push_str(&format!("│ {} v{} :{}\n", n.name, ver, n.port));
131 }
132 }
133 out.push_str("└───────────────────────────────┘\n\n");
134
135 for peer in &data.peers {
136 out.push_str(&format!("┌─ Peer {} ─────────┐\n", peer.id));
137 out.push_str(&format!("│ {}:{}\n", peer.addr, peer.proxy_port));
138 if peer.services.is_empty() {
139 out.push_str("│ (no services)\n");
140 } else {
141 for n in &peer.services {
142 let ver = n.version.as_deref().unwrap_or("?");
143 out.push_str(&format!("│ {} v{}\n", n.name, ver));
144 }
145 }
146 out.push_str("└───────────────────┘\n\n");
147 }
148
149 if !data.peers.is_empty() && data.peers.iter().all(|p| p.services.is_empty()) {
150 out.push_str("(Peers discovered but no services registered)\n");
151 }
152
153 if !data.version_mismatches.is_empty() {
154 out.push_str("\n⚠ Version mismatches:\n");
155 for m in &data.version_mismatches {
156 out.push_str(&format!(" {}\n", m));
157 }
158 }
159
160 out
161}