1use super::catalog::{DeploymentStatus, ModelCatalog};
10use super::gateway::FederationGateway;
11use super::health::{CircuitBreaker, HealthChecker};
12use super::traits::*;
13use ratatui::{
14 layout::{Constraint, Direction, Layout, Rect},
15 style::{Color, Modifier, Style},
16 text::{Line, Span},
17 widgets::{Block, Borders, Cell, Gauge, Paragraph, Row, Table, Tabs},
18 Frame,
19};
20use std::sync::Arc;
21use std::time::Duration;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum FederationTab {
30 #[default]
31 Catalog,
32 Health,
33 Routing,
34 Circuits,
35 Stats,
36 Policies,
37 Help,
38}
39
40impl FederationTab {
41 pub fn titles() -> Vec<&'static str> {
42 vec![
43 "Catalog [1]",
44 "Health [2]",
45 "Routing [3]",
46 "Circuits [4]",
47 "Stats [5]",
48 "Policies [6]",
49 "Help [?]",
50 ]
51 }
52
53 pub fn index(self) -> usize {
54 match self {
55 FederationTab::Catalog => 0,
56 FederationTab::Health => 1,
57 FederationTab::Routing => 2,
58 FederationTab::Circuits => 3,
59 FederationTab::Stats => 4,
60 FederationTab::Policies => 5,
61 FederationTab::Help => 6,
62 }
63 }
64
65 pub fn from_index(index: usize) -> Self {
66 match index {
67 0 => FederationTab::Catalog,
68 1 => FederationTab::Health,
69 2 => FederationTab::Routing,
70 3 => FederationTab::Circuits,
71 4 => FederationTab::Stats,
72 5 => FederationTab::Policies,
73 _ => FederationTab::Help,
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct RoutingRecord {
81 pub request_id: String,
82 pub capability: String,
83 pub selected_node: String,
84 pub score: f64,
85 pub reason: String,
86 pub timestamp: std::time::Instant,
87}
88
89#[derive(Debug, Clone)]
91pub struct CircuitDisplay {
92 pub node_id: String,
93 pub state: CircuitState,
94 pub failure_count: u32,
95 pub last_failure: Option<std::time::Instant>,
96 pub reset_remaining: Option<Duration>,
97}
98
99#[derive(Debug, Clone)]
101pub struct PolicyDisplay {
102 pub name: String,
103 pub weight: f64,
104 pub enabled: bool,
105 pub description: String,
106}
107
108pub struct FederationApp {
110 pub current_tab: FederationTab,
112 pub catalog: Arc<ModelCatalog>,
114 pub health: Arc<HealthChecker>,
116 pub circuit_breaker: Arc<CircuitBreaker>,
118 pub gateway: Option<Arc<FederationGateway>>,
120 pub routing_history: Vec<RoutingRecord>,
122 pub max_history: usize,
124 pub policies: Vec<PolicyDisplay>,
126 pub should_quit: bool,
128 pub status_message: Option<String>,
130 pub selected_row: usize,
132}
133
134impl FederationApp {
135 pub fn new(
137 catalog: Arc<ModelCatalog>,
138 health: Arc<HealthChecker>,
139 circuit_breaker: Arc<CircuitBreaker>,
140 ) -> Self {
141 let policies = vec![
142 PolicyDisplay {
143 name: "health".to_string(),
144 weight: 2.0,
145 enabled: true,
146 description: "Strongly penalize unhealthy nodes".to_string(),
147 },
148 PolicyDisplay {
149 name: "latency".to_string(),
150 weight: 1.0,
151 enabled: true,
152 description: "Prefer low-latency nodes".to_string(),
153 },
154 PolicyDisplay {
155 name: "privacy".to_string(),
156 weight: 1.0,
157 enabled: true,
158 description: "Enforce data sovereignty".to_string(),
159 },
160 PolicyDisplay {
161 name: "locality".to_string(),
162 weight: 1.0,
163 enabled: true,
164 description: "Prefer same-region nodes".to_string(),
165 },
166 PolicyDisplay {
167 name: "cost".to_string(),
168 weight: 1.0,
169 enabled: true,
170 description: "Balance cost vs performance".to_string(),
171 },
172 ];
173
174 Self {
175 current_tab: FederationTab::default(),
176 catalog,
177 health,
178 circuit_breaker,
179 gateway: None,
180 routing_history: Vec::new(),
181 max_history: 100,
182 policies,
183 should_quit: false,
184 status_message: None,
185 selected_row: 0,
186 }
187 }
188
189 #[must_use]
191 pub fn with_gateway(mut self, gateway: Arc<FederationGateway>) -> Self {
192 self.gateway = Some(gateway);
193 self
194 }
195
196 pub fn next_tab(&mut self) {
198 let next = (self.current_tab.index() + 1) % FederationTab::titles().len();
199 self.current_tab = FederationTab::from_index(next);
200 self.selected_row = 0;
201 }
202
203 pub fn prev_tab(&mut self) {
205 let len = FederationTab::titles().len();
206 let prev = (self.current_tab.index() + len - 1) % len;
207 self.current_tab = FederationTab::from_index(prev);
208 self.selected_row = 0;
209 }
210
211 pub fn select_next(&mut self) {
213 self.selected_row = self.selected_row.saturating_add(1);
214 }
215
216 pub fn select_prev(&mut self) {
218 self.selected_row = self.selected_row.saturating_sub(1);
219 }
220
221 pub fn record_routing(&mut self, record: RoutingRecord) {
223 self.routing_history.push(record);
224 if self.routing_history.len() > self.max_history {
225 self.routing_history.remove(0);
226 }
227 }
228
229 pub fn healthy_node_count(&self) -> usize {
231 self.health.healthy_count()
232 }
233
234 pub fn total_node_count(&self) -> usize {
236 self.health.total_count()
237 }
238
239 pub fn success_rate(&self) -> f64 {
241 self.gateway.as_ref().map_or(1.0, |g| {
242 let stats = g.stats();
243 if stats.total_requests == 0 {
244 1.0
245 } else {
246 stats.successful_requests as f64 / stats.total_requests as f64
247 }
248 })
249 }
250
251 pub fn requests_per_sec(&self) -> f64 {
253 self.gateway
254 .as_ref()
255 .map_or(0.0, |g| g.stats().total_requests as f64 / 60.0) }
257}
258
259pub fn render_federation_dashboard(f: &mut Frame<'_>, app: &FederationApp) {
265 let chunks = Layout::default()
266 .direction(Direction::Vertical)
267 .constraints([
268 Constraint::Length(3), Constraint::Length(3), Constraint::Min(0), Constraint::Length(1), ])
273 .split(f.area());
274
275 render_title(f, chunks[0], app);
276 render_tabs(f, chunks[1], app);
277
278 match app.current_tab {
279 FederationTab::Catalog => render_catalog(f, chunks[2], app),
280 FederationTab::Health => render_health(f, chunks[2], app),
281 FederationTab::Routing => render_routing(f, chunks[2], app),
282 FederationTab::Circuits => render_circuits(f, chunks[2], app),
283 FederationTab::Stats => render_stats(f, chunks[2], app),
284 FederationTab::Policies => render_policies(f, chunks[2], app),
285 FederationTab::Help => render_help(f, chunks[2]),
286 }
287
288 render_status_bar(f, chunks[3], app);
289}
290
291fn render_title(f: &mut Frame<'_>, area: Rect, app: &FederationApp) {
292 let healthy = app.healthy_node_count();
293 let total = app.total_node_count();
294
295 let title = format!(
296 " Federation Gateway v1.0 │ {}/{} nodes healthy │ {:.1}% success ",
297 healthy,
298 total,
299 app.success_rate() * 100.0
300 );
301
302 let block = Block::default()
303 .borders(Borders::ALL)
304 .style(Style::default().fg(Color::Cyan))
305 .title(Span::styled(
306 title,
307 Style::default()
308 .fg(Color::White)
309 .add_modifier(Modifier::BOLD),
310 ));
311
312 f.render_widget(block, area);
313}
314
315fn render_tabs(f: &mut Frame<'_>, area: Rect, app: &FederationApp) {
316 let titles: Vec<Line<'_>> = FederationTab::titles()
317 .iter()
318 .map(|t| Line::from(*t))
319 .collect();
320
321 let tabs = Tabs::new(titles)
322 .block(Block::default().borders(Borders::ALL).title("Navigation"))
323 .select(app.current_tab.index())
324 .style(Style::default().fg(Color::White))
325 .highlight_style(
326 Style::default()
327 .fg(Color::Yellow)
328 .add_modifier(Modifier::BOLD),
329 );
330
331 f.render_widget(tabs, area);
332}
333
334fn render_catalog(f: &mut Frame<'_>, area: Rect, app: &FederationApp) {
335 let block = Block::default()
336 .borders(Borders::ALL)
337 .title(" MODEL CATALOG ");
338
339 let entries = app.catalog.all_entries();
340
341 if entries.is_empty() {
342 let no_models =
343 Paragraph::new("No models registered. Use 'apr federation register' to add models.")
344 .style(Style::default().fg(Color::Yellow))
345 .block(block);
346 f.render_widget(no_models, area);
347 return;
348 }
349
350 let header = Row::new(vec![
351 Cell::from("Model").style(Style::default().fg(Color::Cyan)),
352 Cell::from("Node").style(Style::default().fg(Color::Cyan)),
353 Cell::from("Region").style(Style::default().fg(Color::Cyan)),
354 Cell::from("Capabilities").style(Style::default().fg(Color::Cyan)),
355 Cell::from("Status").style(Style::default().fg(Color::Cyan)),
356 ])
357 .height(1)
358 .bottom_margin(1);
359
360 let rows: Vec<Row<'_>> = entries
361 .iter()
362 .flat_map(|entry| {
363 entry.deployments.iter().map(move |dep| {
364 let caps: String = entry
365 .metadata
366 .capabilities
367 .iter()
368 .map(|c| format!("{:?}", c))
369 .collect::<Vec<_>>()
370 .join(", ");
371
372 let status_style = match dep.status {
373 DeploymentStatus::Ready => Style::default().fg(Color::Green),
374 DeploymentStatus::Loading => Style::default().fg(Color::Yellow),
375 DeploymentStatus::Draining => Style::default().fg(Color::Yellow),
376 DeploymentStatus::Removed => Style::default().fg(Color::Red),
377 };
378
379 Row::new(vec![
380 Cell::from(entry.metadata.model_id.0.clone()),
381 Cell::from(dep.node_id.0.clone()),
382 Cell::from(dep.region_id.0.clone()),
383 Cell::from(caps),
384 Cell::from(format!("{:?}", dep.status)).style(status_style),
385 ])
386 })
387 })
388 .collect();
389
390 let table = Table::new(
391 rows,
392 [
393 Constraint::Percentage(20),
394 Constraint::Percentage(20),
395 Constraint::Percentage(15),
396 Constraint::Percentage(30),
397 Constraint::Percentage(15),
398 ],
399 )
400 .header(header)
401 .block(block);
402
403 f.render_widget(table, area);
404}
405
406include!("rendering.rs");
407include!("tui_03.rs");