1pub mod analytics;
5pub mod audit;
6pub mod behavioral_cloning;
7pub mod chains;
8pub mod chaos;
9pub mod config;
10pub mod conformance;
11pub mod contract_diff;
12pub mod dashboard;
13pub mod federation;
14pub mod fixtures;
15pub mod health;
16pub mod import;
17pub mod logs;
18pub mod metrics;
19pub mod plugins;
20pub mod recorder;
21pub mod routes;
22pub mod smoke_tests;
23pub mod time_travel;
24pub mod verification;
25pub mod workspaces;
26pub mod world_state;
27
28use crossterm::event::KeyEvent;
29use ratatui::layout::Rect;
30use ratatui::Frame;
31
32use crate::api::client::MockForgeClient;
33use crate::event::Event;
34use tokio::sync::mpsc;
35
36pub trait Screen: Send {
38 fn title(&self) -> &str;
40
41 fn handle_key(&mut self, key: KeyEvent) -> bool;
43
44 fn render(&self, frame: &mut Frame, area: Rect);
46
47 fn tick(&mut self, client: &MockForgeClient, tx: &mpsc::UnboundedSender<Event>);
50
51 fn on_data(&mut self, payload: &str);
53
54 fn on_error(&mut self, message: &str);
56
57 fn status_hint(&self) -> &str {
59 ""
60 }
61
62 fn error(&self) -> Option<&str> {
65 None
66 }
67
68 fn force_refresh(&mut self) {}
70
71 fn push_log_line(&mut self, _line: String) {}
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ScreenId {
78 Dashboard,
79 Logs,
80 Routes,
81 Metrics,
82 Config,
83 Chaos,
84 Workspaces,
85 Plugins,
86 Fixtures,
87 Health,
88 SmokeTests,
89 TimeTravel,
90 Chains,
91 Verification,
92 Analytics,
93 Recorder,
94 Import,
95 Audit,
96 WorldState,
97 ContractDiff,
98 Federation,
99 BehavioralCloning,
100 Conformance,
101}
102
103impl ScreenId {
104 pub const ALL: &[Self] = &[
106 Self::Dashboard,
107 Self::Logs,
108 Self::Routes,
109 Self::Metrics,
110 Self::Config,
111 Self::Chaos,
112 Self::Workspaces,
113 Self::Plugins,
114 Self::Fixtures,
115 Self::Health,
116 Self::SmokeTests,
117 Self::TimeTravel,
118 Self::Chains,
119 Self::Conformance,
126 Self::Verification,
127 Self::Analytics,
128 Self::Recorder,
129 Self::Import,
130 Self::Audit,
131 Self::WorldState,
132 Self::ContractDiff,
133 Self::Federation,
134 Self::BehavioralCloning,
135 ];
136
137 pub fn label(self) -> &'static str {
138 match self {
139 Self::Dashboard => "Dashboard",
140 Self::Logs => "Logs",
141 Self::Routes => "Routes",
142 Self::Metrics => "Metrics",
143 Self::Config => "Config",
144 Self::Chaos => "Chaos",
145 Self::Workspaces => "Workspaces",
146 Self::Plugins => "Plugins",
147 Self::Fixtures => "Fixtures",
148 Self::Health => "Health",
149 Self::SmokeTests => "Smoke Tests",
150 Self::TimeTravel => "Time Travel",
151 Self::Chains => "Chains",
152 Self::Verification => "Verification",
153 Self::Analytics => "Analytics",
154 Self::Recorder => "Recorder",
155 Self::Import => "Import",
156 Self::Audit => "Audit",
157 Self::WorldState => "World State",
158 Self::ContractDiff => "Contract Diff",
159 Self::Federation => "Federation",
160 Self::BehavioralCloning => "VBR",
161 Self::Conformance => "Conformance",
162 }
163 }
164
165 pub fn data_key(self) -> &'static str {
166 match self {
167 Self::Dashboard => "dashboard",
168 Self::Logs => "logs",
169 Self::Routes => "routes",
170 Self::Metrics => "metrics",
171 Self::Config => "config",
172 Self::Chaos => "chaos",
173 Self::Workspaces => "workspaces",
174 Self::Plugins => "plugins",
175 Self::Fixtures => "fixtures",
176 Self::Health => "health",
177 Self::SmokeTests => "smoke_tests",
178 Self::TimeTravel => "time_travel",
179 Self::Chains => "chains",
180 Self::Verification => "verification",
181 Self::Analytics => "analytics",
182 Self::Recorder => "recorder",
183 Self::Import => "import",
184 Self::Audit => "audit",
185 Self::WorldState => "world_state",
186 Self::ContractDiff => "contract_diff",
187 Self::Federation => "federation",
188 Self::BehavioralCloning => "behavioral_cloning",
189 Self::Conformance => "conformance",
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn all_has_correct_count() {
200 assert_eq!(ScreenId::ALL.len(), 23);
202 }
203
204 #[test]
205 fn all_labels_are_non_empty() {
206 for screen_id in ScreenId::ALL {
207 let label = screen_id.label();
208 assert!(!label.is_empty(), "label() returned empty string for {screen_id:?}");
209 }
210 }
211
212 #[test]
213 fn all_data_keys_are_non_empty() {
214 for screen_id in ScreenId::ALL {
215 let key = screen_id.data_key();
216 assert!(!key.is_empty(), "data_key() returned empty string for {screen_id:?}");
217 }
218 }
219
220 #[test]
221 fn all_data_keys_are_snake_case() {
222 for screen_id in ScreenId::ALL {
223 let key = screen_id.data_key();
224 assert!(
225 !key.contains(' ') && !key.contains('-'),
226 "data_key() for {screen_id:?} contains spaces or hyphens: {key}"
227 );
228 assert_eq!(
229 key,
230 key.to_lowercase(),
231 "data_key() for {screen_id:?} is not lowercase: {key}"
232 );
233 }
234 }
235
236 #[test]
237 fn all_data_keys_are_unique() {
238 let keys: Vec<&str> = ScreenId::ALL.iter().map(|s| s.data_key()).collect();
239 let mut deduped = keys.clone();
240 deduped.sort_unstable();
241 deduped.dedup();
242 assert_eq!(keys.len(), deduped.len(), "Duplicate data_key() values found");
243 }
244
245 #[test]
246 fn all_labels_and_data_keys_consistent_count() {
247 let label_count = ScreenId::ALL.iter().filter(|s| !s.label().is_empty()).count();
248 let key_count = ScreenId::ALL.iter().filter(|s| !s.data_key().is_empty()).count();
249 assert_eq!(label_count, key_count);
250 assert_eq!(label_count, ScreenId::ALL.len());
251 }
252
253 #[test]
254 fn specific_labels() {
255 assert_eq!(ScreenId::Dashboard.label(), "Dashboard");
256 assert_eq!(ScreenId::Logs.label(), "Logs");
257 assert_eq!(ScreenId::SmokeTests.label(), "Smoke Tests");
258 assert_eq!(ScreenId::TimeTravel.label(), "Time Travel");
259 assert_eq!(ScreenId::WorldState.label(), "World State");
260 assert_eq!(ScreenId::ContractDiff.label(), "Contract Diff");
261 assert_eq!(ScreenId::BehavioralCloning.label(), "VBR");
262 }
263
264 #[test]
265 fn specific_data_keys() {
266 assert_eq!(ScreenId::Dashboard.data_key(), "dashboard");
267 assert_eq!(ScreenId::SmokeTests.data_key(), "smoke_tests");
268 assert_eq!(ScreenId::TimeTravel.data_key(), "time_travel");
269 assert_eq!(ScreenId::WorldState.data_key(), "world_state");
270 assert_eq!(ScreenId::ContractDiff.data_key(), "contract_diff");
271 assert_eq!(ScreenId::BehavioralCloning.data_key(), "behavioral_cloning");
272 }
273
274 #[test]
275 fn screen_id_equality() {
276 assert_eq!(ScreenId::Dashboard, ScreenId::Dashboard);
277 assert_ne!(ScreenId::Dashboard, ScreenId::Logs);
278 }
279
280 #[test]
281 fn screen_id_clone() {
282 let id = ScreenId::Dashboard;
283 let cloned = id;
284 assert_eq!(id, cloned);
285 }
286}