Skip to content

Commit b1ad717

Browse files
feat: wire container logs into TUI dashboard via control socket
The TUI Logs tab showed "not available" for all services because refresh_data() never populated the logs HashMap. Now refresh_logs() connects to the control socket (same mechanism as `vz stack logs`) and fetches `tail -n 200 /var/log/vz-oci/output.log` for the currently-selected service when on the Logs tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 554b9eb commit b1ad717

2 files changed

Lines changed: 73 additions & 15 deletions

File tree

crates/vz-cli/src/commands/stack.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ async fn cmd_up(args: UpArgs) -> anyhow::Result<()> {
654654
let tui_spec = spec.clone();
655655
let tui_name = spec.name.clone();
656656
let tui_db = db_path.clone();
657-
crate::tui::run_tui(tui_name, tui_spec, tui_db)?;
657+
crate::tui::run_tui(tui_name, tui_spec, tui_db, Some(sock_path.clone()))?;
658658
} else {
659659
serve_control_socket(&sock_path, &spec, &mut orchestrator).await?;
660660
}
@@ -1743,7 +1743,8 @@ async fn cmd_dashboard(args: DashboardArgs) -> anyhow::Result<()> {
17431743
})?
17441744
};
17451745

1746-
crate::tui::run_tui(args.name, spec, db_path)
1746+
let sock_path = state_dir.join("control.sock");
1747+
crate::tui::run_tui(args.name, spec, db_path, Some(sock_path))
17471748
}
17481749

17491750
// ── Helpers ────────────────────────────────────────────────────────

crates/vz-cli/src/tui.rs

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
//! mode) or standalone via `vz stack dashboard <name>`.
77
88
use std::collections::HashMap;
9-
use std::io::{self, IsTerminal, Stdout};
9+
use std::io::{self, BufRead, IsTerminal, Stdout, Write as _};
10+
use std::os::unix::net::UnixStream;
1011
use std::path::PathBuf;
1112
use std::time::{Duration, Instant};
1213

@@ -82,6 +83,7 @@ pub struct App {
8283
logs: HashMap<String, String>,
8384
selected_log_service: usize,
8485
log_scroll: usize,
86+
sock_path: Option<PathBuf>,
8587

8688
// UI state
8789
show_help: bool,
@@ -93,7 +95,7 @@ pub struct App {
9395

9496
impl App {
9597
/// Create a new TUI application.
96-
pub fn new(stack_name: String, spec: StackSpec, store: StateStore) -> Self {
98+
pub fn new(stack_name: String, spec: StackSpec, store: StateStore, sock_path: Option<PathBuf>) -> Self {
9799
let service_names: Vec<String> = spec.services.iter().map(|s| s.name.clone()).collect();
98100
let logs: HashMap<String, String> = service_names
99101
.iter()
@@ -114,6 +116,7 @@ impl App {
114116
logs,
115117
selected_log_service: 0,
116118
log_scroll: 0,
119+
sock_path,
117120
show_help: false,
118121
should_quit: false,
119122
recent_events: Vec::new(),
@@ -146,6 +149,55 @@ impl App {
146149
self.event_scroll = self.events.len().saturating_sub(1);
147150
}
148151
}
152+
153+
// Fetch logs only when viewing the Logs tab (avoid unnecessary socket calls).
154+
if self.active_tab == Tab::Logs {
155+
self.refresh_logs();
156+
}
157+
}
158+
159+
/// Fetch logs for the currently-selected service via the control socket.
160+
fn refresh_logs(&mut self) {
161+
let sock_path = match &self.sock_path {
162+
Some(p) if p.exists() => p.clone(),
163+
_ => return,
164+
};
165+
166+
let service_name = match self.current_log_service_name() {
167+
Some(n) => n,
168+
None => return,
169+
};
170+
171+
let stream = match UnixStream::connect(&sock_path) {
172+
Ok(s) => s,
173+
Err(_) => return,
174+
};
175+
let _ = stream.set_read_timeout(Some(Duration::from_millis(500)));
176+
let _ = stream.set_write_timeout(Some(Duration::from_millis(200)));
177+
178+
let request = serde_json::json!({
179+
"action": "exec",
180+
"service": service_name,
181+
"cmd": ["tail", "-n", "200", "/var/log/vz-oci/output.log"]
182+
});
183+
184+
let mut request_bytes = serde_json::to_vec(&request).unwrap_or_default();
185+
request_bytes.push(b'\n');
186+
187+
if (&stream).write_all(&request_bytes).is_err() {
188+
return;
189+
}
190+
191+
let mut reader = io::BufReader::new(&stream);
192+
let mut line = String::new();
193+
if reader.read_line(&mut line).is_ok() && !line.is_empty() {
194+
if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&line) {
195+
let stdout = resp.get("stdout").and_then(|v| v.as_str()).unwrap_or("");
196+
if !stdout.is_empty() {
197+
self.logs.insert(service_name, stdout.to_string());
198+
}
199+
}
200+
}
149201
}
150202

151203
/// Handle a key press event.
@@ -402,7 +454,12 @@ pub fn is_tty() -> bool {
402454
/// This takes over the terminal and presents a dashboard that polls
403455
/// the state store at `db_path` every 500ms. Returns when the user
404456
/// presses `q` or `Ctrl-C`.
405-
pub fn run_tui(stack_name: String, spec: StackSpec, db_path: PathBuf) -> anyhow::Result<()> {
457+
pub fn run_tui(
458+
stack_name: String,
459+
spec: StackSpec,
460+
db_path: PathBuf,
461+
sock_path: Option<PathBuf>,
462+
) -> anyhow::Result<()> {
406463
enable_raw_mode().context("failed to enable raw terminal mode")?;
407464
let mut stdout = io::stdout();
408465
execute!(stdout, EnterAlternateScreen).context("failed to enter alternate screen")?;
@@ -414,7 +471,7 @@ pub fn run_tui(stack_name: String, spec: StackSpec, db_path: PathBuf) -> anyhow:
414471

415472
let store =
416473
StateStore::open(&db_path).context("failed to open state store for TUI dashboard")?;
417-
let mut app = App::new(stack_name, spec, store);
474+
let mut app = App::new(stack_name, spec, store, sock_path);
418475

419476
// Initial data load.
420477
app.refresh_data();
@@ -1152,7 +1209,7 @@ mod tests {
11521209
fn app_new_initializes_correctly() {
11531210
let spec = make_test_spec();
11541211
let store = StateStore::in_memory().unwrap();
1155-
let app = App::new("test".into(), spec, store);
1212+
let app = App::new("test".into(), spec, store, None);
11561213

11571214
assert_eq!(app.stack_name, "test");
11581215
assert_eq!(app.active_tab, Tab::Services);
@@ -1167,7 +1224,7 @@ mod tests {
11671224
fn handle_key_quit() {
11681225
let spec = make_test_spec();
11691226
let store = StateStore::in_memory().unwrap();
1170-
let mut app = App::new("test".into(), spec, store);
1227+
let mut app = App::new("test".into(), spec, store, None);
11711228

11721229
app.handle_key(event::KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE));
11731230
assert!(app.should_quit);
@@ -1177,7 +1234,7 @@ mod tests {
11771234
fn handle_key_ctrl_c() {
11781235
let spec = make_test_spec();
11791236
let store = StateStore::in_memory().unwrap();
1180-
let mut app = App::new("test".into(), spec, store);
1237+
let mut app = App::new("test".into(), spec, store, None);
11811238

11821239
app.handle_key(event::KeyEvent::new(
11831240
KeyCode::Char('c'),
@@ -1190,7 +1247,7 @@ mod tests {
11901247
fn handle_key_tab_switch() {
11911248
let spec = make_test_spec();
11921249
let store = StateStore::in_memory().unwrap();
1193-
let mut app = App::new("test".into(), spec, store);
1250+
let mut app = App::new("test".into(), spec, store, None);
11941251

11951252
assert_eq!(app.active_tab, Tab::Services);
11961253

@@ -1208,7 +1265,7 @@ mod tests {
12081265
fn handle_key_number_tabs() {
12091266
let spec = make_test_spec();
12101267
let store = StateStore::in_memory().unwrap();
1211-
let mut app = App::new("test".into(), spec, store);
1268+
let mut app = App::new("test".into(), spec, store, None);
12121269

12131270
app.handle_key(event::KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE));
12141271
assert_eq!(app.active_tab, Tab::Events);
@@ -1224,7 +1281,7 @@ mod tests {
12241281
fn handle_key_help_toggle() {
12251282
let spec = make_test_spec();
12261283
let store = StateStore::in_memory().unwrap();
1227-
let mut app = App::new("test".into(), spec, store);
1284+
let mut app = App::new("test".into(), spec, store, None);
12281285

12291286
app.handle_key(event::KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
12301287
assert!(app.show_help);
@@ -1238,7 +1295,7 @@ mod tests {
12381295
fn navigate_services() {
12391296
let spec = make_test_spec();
12401297
let store = StateStore::in_memory().unwrap();
1241-
let mut app = App::new("test".into(), spec, store);
1298+
let mut app = App::new("test".into(), spec, store, None);
12421299

12431300
// Add some observed services.
12441301
app.services = vec![
@@ -1279,7 +1336,7 @@ mod tests {
12791336
fn navigate_jump_top_bottom() {
12801337
let spec = make_test_spec();
12811338
let store = StateStore::in_memory().unwrap();
1282-
let mut app = App::new("test".into(), spec, store);
1339+
let mut app = App::new("test".into(), spec, store, None);
12831340

12841341
app.services = vec![
12851342
ServiceObservedState {
@@ -1338,7 +1395,7 @@ mod tests {
13381395
)
13391396
.unwrap();
13401397

1341-
let mut app = App::new("test".into(), spec, store);
1398+
let mut app = App::new("test".into(), spec, store, None);
13421399
app.refresh_data();
13431400

13441401
assert_eq!(app.services.len(), 1);

0 commit comments

Comments
 (0)