Skip to content

feat: v0.2 Docker container integration#1

Merged
heki1224 merged 8 commits intomasterfrom
feature/v0.2-docker
Mar 27, 2026
Merged

feat: v0.2 Docker container integration#1
heki1224 merged 8 commits intomasterfrom
feature/v0.2-docker

Conversation

@heki1224
Copy link
Copy Markdown
Owner

Summary

  • Add Docker container monitoring panel (4th pane, auto-hides when Docker unavailable)
  • Display container name, status, CPU%, and memory usage via bollard polling
  • CPU% calculated from delta of cumulative stats (per Docker API spec)

Test Plan

  • cargo test passes (13 tests)
  • With Docker running: 4-pane layout shows container list
  • Without Docker / permission denied: 3-pane layout (existing behavior unchanged)
  • Containers appear/disappear dynamically as Docker starts/stops

🤖 Generated with Claude Code

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates devtop to version 0.2.0, introducing a new Docker container monitoring panel. The implementation includes a Docker collector using the bollard library to gather real-time CPU and memory statistics, which are then displayed in a dynamic TUI panel that auto-hides when Docker is unavailable. My feedback focuses on improving the robustness of the Docker integration, specifically by making the collector resilient to daemon connection failures and ensuring the UI correctly reflects Docker's availability state. Additionally, there are opportunities to optimize UI performance by reducing string allocations and improving code maintainability by replacing magic numbers with constants.

Comment on lines +58 to +61
CollectorMessage::Docker(containers) => {
self.docker_available = true;
self.containers = containers;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The docker_available flag is set to true upon receiving the first Docker message and is never reset. If the Docker daemon stops, the collector will likely start sending empty lists of containers, but docker_available will remain true. This results in an empty Docker panel being displayed instead of being hidden.

To correctly reflect Docker's availability, the CollectorMessage::Docker could be changed to carry a Result, allowing you to set docker_available to false upon receiving an error.

Comment on lines +26 to +44
pub async fn start(tx: mpsc::Sender<CollectorMessage>) {
let docker = match Docker::connect_with_local_defaults() {
Ok(d) => d,
Err(_) => return,
};
if docker.ping().await.is_err() {
return;
}

let mut prev_stats: HashMap<String, (u64, u64)> = HashMap::new();

loop {
let containers = collect_containers(&docker, &mut prev_stats).await;
if tx.send(CollectorMessage::Docker(containers)).await.is_err() {
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Docker collector's start function is not resilient to connection failures.

  1. Startup Failure: If the Docker daemon is not running when devtop starts, the collector exits permanently and will not detect if Docker becomes available later.
  2. Runtime Failure: If the connection to the Docker daemon is lost while devtop is running, collect_containers suppresses the error and returns an empty list. The start function continues to loop, sending empty lists to the UI, but it never attempts to re-establish the connection.

To align with the 'dynamic' behavior described in the PR, the collector should be refactored to handle these failures gracefully. It should periodically attempt to connect/reconnect to the Docker daemon and propagate connection errors so the UI can be updated accordingly (i.e., hide the Docker panel).

Comment on lines +54 to +57
let list = match docker.list_containers(Some(options)).await {
Ok(v) => v,
Err(_) => return Vec::new(),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Errors from docker.list_containers are currently being suppressed, and an empty vector is returned instead. This makes it impossible for the caller to distinguish between a scenario with no running containers and an error scenario (e.g., Docker daemon stopped). This can lead to the UI showing an empty but visible Docker panel when it should be hidden because Docker is unavailable.

Consider propagating the error from this function, for example by changing the return type to Result<Vec<ContainerInfo>, bollard::errors::Error>. The caller can then handle the error appropriately, for instance by attempting to reconnect to the Docker daemon.

Comment on lines +34 to +35
Cell::from(c.name.clone()),
Cell::from(c.status.clone()).style(status_style),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The name and status strings are being cloned for each render cycle. This is inefficient as rendering happens frequently. You can avoid these clones by passing string slices to Cell::from.

Cell::from can accept a &str, so you can pass &c.name and &c.status directly.

Suggested change
Cell::from(c.name.clone()),
Cell::from(c.status.clone()).style(status_style),
Cell::from(&c.name),
Cell::from(&c.status).style(status_style),

Comment on lines +54 to +64
fn format_bytes(bytes: u64) -> String {
if bytes >= 1024 * 1024 * 1024 {
format!("{:.1}GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else if bytes >= 1024 * 1024 {
format!("{:.0}MB", bytes as f64 / (1024.0 * 1024.0))
} else if bytes >= 1024 {
format!("{:.0}KB", bytes as f64 / 1024.0)
} else {
format!("{}B", bytes)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The format_bytes function uses magic numbers for byte conversions. To improve readability and maintainability, it's better to define constants for kilobyte, megabyte, and gigabyte thresholds.

This also makes the logic for unit conversion clearer.

Suggested change
fn format_bytes(bytes: u64) -> String {
if bytes >= 1024 * 1024 * 1024 {
format!("{:.1}GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else if bytes >= 1024 * 1024 {
format!("{:.0}MB", bytes as f64 / (1024.0 * 1024.0))
} else if bytes >= 1024 {
format!("{:.0}KB", bytes as f64 / 1024.0)
} else {
format!("{}B", bytes)
}
}
const KB: u64 = 1024;
const MB: u64 = 1024 * 1024;
const GB: u64 = 1024 * 1024 * 1024;
fn format_bytes(bytes: u64) -> String {
if bytes >= GB {
format!("{:.1}GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.0}MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.0}KB", bytes as f64 / KB as f64)
} else {
format!("{}B", bytes)
}
}

@heki1224 heki1224 merged commit f6e5de9 into master Mar 27, 2026
2 checks passed
@heki1224 heki1224 deleted the feature/v0.2-docker branch March 27, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant