dtop 0.6.13

A terminal-based Docker container monitoring tool with real-time CPU and memory metrics
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

`dtop` is a terminal-based Docker container monitoring tool built with Rust. It provides real-time CPU, memory, and network metrics for Docker containers through a TUI interface, with support for both local and remote (SSH/TCP) Docker daemons. The tool supports **monitoring multiple Docker hosts simultaneously** and includes a **built-in log viewer** for streaming container logs.

## Build & Run Commands

```bash
# Development
cargo run                                    # Run with local Docker daemon (or config file)
cargo run -- --host ssh://user@host         # Run with remote Docker host via SSH
cargo run -- --host tcp://host:2375         # Run with remote Docker host via TCP
cargo run -- --host tls://host:2376         # Run with remote Docker host via TLS
cargo run -- --host local --host ssh://user@host1 --host tcp://host2:2375  # Multiple hosts
cargo run -- --filter status=running        # Filter to show only running containers
cargo run -- --filter name=nginx --filter label=env=prod  # Multiple filters
cargo run -- --all                           # Show all containers (including stopped/exited)
cargo run -- -a                              # Short version of --all
cargo run -- --sort name                     # Sort containers by name
cargo run -- -s cpu                          # Sort containers by CPU usage

# Self-update
cargo run -- update                          # Update dtop to the latest version
dtop update                                  # (or use the installed binary)

# Testing
cargo test                                   # Run all tests
cargo test -- --nocapture                    # Run tests with output
cargo insta test                             # Run tests with snapshot review
cargo insta accept                           # Accept all pending snapshots
cargo insta reject                           # Reject all pending snapshots

# Production build
cargo build --release                        # The binary will be at target/release/dtop (includes self-update)
cargo build --release --no-default-features  # Build without self-update feature (smaller binary)

# Changelog generation
git-cliff --latest                           # Generate changelog for the latest tag
git-cliff --unreleased                       # Generate changelog for unreleased changes
git-cliff --tag v0.3.0..v0.3.6              # Generate changelog for a version range
git-cliff -o CHANGELOG.md                    # Write changelog to file

# Docker build
docker build -t dtop .
docker run -v /var/run/docker.sock:/var/run/docker.sock -it dtop

# Nix
nix run .                                    # Run using pre-built binary (fast)
nix run .#source                             # Run building from source
nix build                                    # Build the package
nix flake check                              # Verify flake is valid
nix develop                                  # Enter dev shell with Rust tooling
```

## Nix Flake

The project includes a Nix flake (`flake.nix`) for reproducible builds and easy installation.

### Packages

- `packages.default` - Pre-built binary from GitHub releases (instant install)
- `packages.source` - Build from source using `buildRustPackage`

### Updating Nix Hashes (for new releases)

When releasing a new version, update the flake hashes:

```bash
./scripts/update-nix-hashes.sh <VERSION>
# Example: ./scripts/update-nix-hashes.sh 0.6.8
```

This script requires Nix to be installed. It will:
1. Update the version in `flake.nix`
2. Fetch new release artifacts and compute their hashes
3. Update all platform hashes automatically

Test the updated flake with:
```bash
nix build && ./result/bin/dtop --version
```

## Configuration

The application supports configuration via YAML files. Config files are searched in the following order (first found wins):

1. `./config.yaml` or `./config.yml`
2. `./.dtop.yaml` or `./.dtop.yml`
3. `~/.config/dtop/config.yaml` or `~/.config/dtop/config.yml`
4. `~/.dtop.yaml` or `~/.dtop.yml`

**Command line arguments take precedence over config file values.**

Example config file (`config.yaml`):
```yaml
hosts:
  - host: local
  - host: ssh://user@server1
    filter:
      - status=running
      - label=environment=production
  - host: tcp://192.168.1.100:2375
  - host: ssh://root@146.190.3.114
    dozzle: https://l.dozzle.dev/
    filter:
      - name=nginx
      - ancestor=ubuntu:24.04

# Icon style: "unicode" (default) or "nerd" (requires Nerd Font)
icons: unicode

# Show all containers (default: false, shows only running containers)
# Set to true to show all containers including stopped, exited, and paused containers
all: false

# Default sort field: "uptime" (default), "name", "cpu", or "memory"
sort: uptime
```

Each host entry is a struct with:
- `host`: Docker connection string (required)
- `dozzle`: Optional URL to Dozzle instance
- `filter`: Optional list of Docker filters (e.g., ["status=running", "name=nginx"])
- Future optional fields can be added as needed

Global config options:
- `icons`: Icon style to use ("unicode" or "nerd")
- `all`: Show all containers including stopped/exited (default: false)
- `sort`: Default sort field for container list ("uptime", "name", "cpu", "memory")

See `config.example.yaml` for a complete example.

### Show All Containers

The `--all` / `-a` flag controls whether to show all containers or only running containers:

- **CLI**: `--all` or `-a` flag (boolean, follows `docker ps -a` convention)
- **Config file**: `all: true/false` in YAML

**Behavior:**
- By default, only running containers are shown
- The `--all` flag enables showing all containers (including stopped, exited, paused)
- The flag is **one-way enable only**: it can enable showing all containers but cannot disable it
- If config has `all: true`, the CLI flag cannot override it back to false
- Users can always toggle the view with the 'a' key in the UI

**Examples:**
```bash
dtop --all              # Show all containers
dtop -a                 # Short version
dtop                    # Show running only (unless config has all: true)
```

**Note:** This design matches Docker's `docker ps -a` behavior where the flag is a simple boolean enable.

### Default Sort Field

The `--sort` / `-s` option sets the default sort field for the container list:

- **CLI**: `--sort <field>` or `-s <field>`
- **Config file**: `sort: <field>` in YAML

**Available sort fields:**
- `uptime` (or `u`) - Sort by container creation time (default, newest first)
- `name` (or `n`) - Sort by container name (alphabetically, ascending)
- `cpu` (or `c`) - Sort by CPU usage (highest first)
- `memory` (or `m`) - Sort by memory usage (highest first)

**Behavior:**
- Each field has a default sort direction (uptime/cpu/memory: descending, name: ascending)
- CLI takes precedence over config file
- Users can change the sort field and toggle direction in the UI with 's' or specific keys (u/n/c/m)

**Examples:**
```bash
dtop --sort name        # Sort by name alphabetically
dtop -s cpu             # Sort by CPU usage (highest first)
dtop --sort memory      # Sort by memory usage
```

### Container Filtering

The application supports Docker filters similar to `docker ps --filter`. Filters can be specified via:
- **CLI**: `--filter` or `-f` flag (applies to all hosts)
- **Config file**: Per-host `filter` field in YAML (host-specific filters)

**CLI filters take precedence over config file filters.**

Available filters (container listing):
- `id` - Container ID (full or partial)
- `name` - Container name (supports partial matches)
- `label` - Label key or key=value pair
- `status` - Container state: created, restarting, running, removing, paused, exited, dead
- `ancestor` - Image name/tag or descendant
- `before` - Created before container ID/name
- `since` - Created after container ID/name
- `volume` - Mounted volume or path
- `network` - Connected network
- `publish`/`expose` - Published/exposed ports
- `health` - Healthcheck status: starting, healthy, unhealthy, none
- `exited` - Exit code
- `isolation` - Isolation type (Windows only)
- `is-task` - Service task containers (boolean)

**Filter Logic:**
- Multiple values for same filter = OR logic: `--filter status=running --filter status=paused`
- Different filter types = AND logic: `--filter status=running --filter name=nginx`

**Events API Compatibility:**
Some filters only work with container listing, not the events stream. The application will log warnings for incompatible filters (e.g., `status`, `ancestor`, `health`) that won't apply to real-time event monitoring. Compatible filters for both listing and events include: `label`, `network`, `volume`.

Filters `id` and `name` are automatically mapped to the `container` filter for events API compatibility.

## Architecture

The application follows an **event-driven architecture** with multiple async/threaded components communicating via a single mpsc channel (`AppEvent`). The architecture supports **multi-host monitoring** by spawning independent container managers for each Docker host.

### Source Code Organization

The codebase is organized into logical modules:

```
src/
├── cli/                   # CLI-related modules
│   ├── config.rs         # Configuration file loading (YAML)
│   ├── connect.rs        # Docker host connection and verification
│   ├── filters.rs        # Docker filter parsing (--filter support)
│   └── update.rs         # Self-update functionality
│
├── core/                  # Core application logic
│   ├── app_state/        # Central state manager (modularized)
│   │   ├── mod.rs        # AppState struct and main event dispatcher
│   │   ├── actions.rs    # Action menu handling (start/stop/restart/remove)
│   │   ├── container_events.rs  # Container lifecycle event handlers
│   │   ├── integrations.rs      # Dozzle integration handlers
│   │   ├── log_view.rs   # Log view event handlers
│   │   ├── navigation.rs # Selection and navigation handlers
│   │   ├── search.rs     # Search mode and filtering handlers
│   │   └── sorting.rs    # Container sorting logic
│   └── types.rs          # Core types and events
│
├── docker/                # Docker-related functionality
│   ├── connection.rs     # Container manager & Docker host abstraction
│   ├── logs.rs           # Log streaming
│   ├── stats.rs          # Stats streaming and calculation
│   └── actions.rs        # Container actions (start/stop/restart/remove)
│
├── ui/                    # UI rendering and input handling
│   ├── input.rs          # Keyboard worker
│   ├── render.rs         # Ratatui UI rendering
│   ├── container_list.rs # Container list table rendering
│   ├── action_menu.rs    # Action menu popup rendering
│   ├── help.rs           # Help popup rendering
│   ├── icons.rs          # Icon sets (Unicode and Nerd Font)
│   └── ui_tests.rs       # UI snapshot tests
│
├── lib.rs                # Library root with module declarations
└── main.rs               # Binary entry point
```

### Core Components

1. **Main Event Loop** (`main.rs::run_event_loop`)
   - Receives events from all container managers via a shared channel
   - Delegates state management to `AppState` struct
   - Renders UI at 500ms intervals using Ratatui
   - Uses throttling to wait for events or timeout, then drains all pending events

2. **AppState** (`core/app_state/mod.rs::AppState`)
   - Central state manager that handles all runtime data
   - Maintains container state in `HashMap<ContainerKey, Container>` where `ContainerKey` is `(host_id, container_id)`
   - Manages view state (container list, log view, action menu, search mode)
   - Handles log streaming, scrolling, and auto-scroll behavior
   - Pre-sorts containers by host_id and selected sort field for efficient rendering
   - Single source of truth for container data across all hosts
   - **Modular architecture**: Event handlers are split across specialized modules:
     - `actions.rs`: Action menu navigation and execution
     - `container_events.rs`: Container lifecycle and stats updates
     - `integrations.rs`: Dozzle integration
     - `log_view.rs`: Log streaming and scrolling
     - `navigation.rs`: Selection and navigation
     - `search.rs`: Search filtering and input handling
     - `sorting.rs`: Container sorting by multiple fields

3. **Container Manager** (`docker/connection.rs::container_manager`) - **One per Docker host**
   - Async task that manages Docker API interactions for a specific host
   - Each manager operates independently with its own `DockerHost` instance
   - Fetches initial container list on startup
   - Subscribes to Docker events (start/stop/die) for that host
   - Spawns individual stats stream tasks per container
   - Each container gets its own async task running `stream_container_stats`
   - All events include the `host_id` to identify their source

4. **Stats Streaming** (`docker/stats.rs::stream_container_stats`)
   - One async task per container that streams real-time stats
   - Uses **exponential moving average (alpha=0.3)** to smooth CPU, memory, and network stats
   - Calculates network TX/RX rates in bytes per second
   - CPU calculation: Delta between current and previous usage, normalized by system CPU delta and CPU count
   - Memory calculation: Current usage divided by limit, expressed as percentage

5. **Log Streaming** (`docker/logs.rs::stream_container_logs`)
   - Streams logs from a container in real-time
   - Fetches last 100 lines on startup, then follows new logs
   - Parses timestamps (RFC3339 format) and messages
   - Uses `ansi-to-tui` to parse ANSI escape codes for colored output
   - Preserves whitespace and formatting from original logs
   - Sends each log line as `AppEvent::LogLine` event with pre-parsed Text

6. **Keyboard Worker** (`ui/input.rs::keyboard_worker`)
   - Blocking thread that polls keyboard input every 200ms
   - Handles: 'q'/Ctrl-C (quit), Enter (show action menu/execute), Esc (exit view/cancel), Up/Down (navigate/scroll)
   - Arrow keys: Right (view logs), Left (exit log view), '/' (search mode)
   - Separate thread because crossterm's event polling is blocking

7. **Container Actions** (`docker/actions.rs::execute_container_action`)
   - Async execution of Docker container actions
   - Supports: Start, Stop, Restart, Remove
   - Stop/Restart use 10-second timeout before force kill
   - Remove uses force option to remove even if running
   - Sends progress events (InProgress, Success, Error) back to main event loop

### Multi-Host Architecture

```
Host1 (local)     → container_manager → AppEvent(host_id="local", ...) ┐
Host2 (server1)   → container_manager → AppEvent(host_id="server1", ...)├→ Main Loop → UI
Host3 (server2)   → container_manager → AppEvent(host_id="server2", ...)┘
Keyboard          → keyboard_worker   → AppEvent::Quit → Main Loop → Exit
```

**Key Design Points:**
- Each host runs its own independent `container_manager` task
- All container managers share the same event channel (`mpsc::Sender<AppEvent>`)
- Every event includes a `host_id` to identify which host it came from
- Containers are uniquely identified by `ContainerKey { host_id, container_id }`
- The UI displays host information alongside container information

### Event Types (`core/types.rs::AppEvent`)

Container-related events use structured types to identify containers across hosts:

- `InitialContainerList(HostId, Vec<Container>)` - Batch of containers from a specific host on startup
- `ContainerCreated(Container)` - New container started (host_id is in the Container struct)
- `ContainerDestroyed(ContainerKey)` - Container stopped/died (identified by host_id + container_id)
- `ContainerStat(ContainerKey, ContainerStats)` - Stats update (identified by host_id + container_id)
- `ContainerHealthChanged(ContainerKey, HealthStatus)` - Health status changed for a container
- `Quit` - User pressed 'q' or Ctrl-C
- `Resize` - Terminal was resized
- `SelectPrevious` - Move selection up (Up arrow in container list)
- `SelectNext` - Move selection down (Down arrow in container list)
- `EnterPressed` - User pressed Enter to show action menu or execute action
- `ExitLogView` - User pressed Left arrow to exit log view
- `ShowLogView` - User pressed Right arrow to view logs
- `ScrollUp` - Scroll up in log view (Up arrow)
- `ScrollDown` - Scroll down in log view (Down arrow)
- `LogLine(ContainerKey, LogEntry)` - New log line received from streaming logs
- `OpenDozzle` - User pressed 'o' to open Dozzle for selected container
- `ToggleHelp` - User pressed '?' to toggle help popup
- `CycleSortField` - User pressed 's' to cycle through sort fields
- `SetSortField(SortField)` - User pressed a specific key to set sort field (u/n/c/m)
- `ToggleShowAll` - User pressed 'a' to toggle showing all containers (including stopped)
- `CancelActionMenu` - User pressed Esc to cancel action menu or exit views
- `SelectActionUp` - Navigate up in action menu (Up arrow)
- `SelectActionDown` - Navigate down in action menu (Down arrow)
- `ActionInProgress(ContainerKey, ContainerAction)` - Container action started
- `ActionSuccess(ContainerKey, ContainerAction)` - Container action completed successfully
- `ActionError(ContainerKey, ContainerAction, String)` - Container action failed
- `EnterSearchMode` - User pressed '/' to enter search mode
- `SearchKeyEvent(KeyEvent)` - Key event for search input (passed to tui-input)

### View States (`core/types.rs::ViewState`)

The application has four view states:
- `ContainerList` - Main view showing all containers across all hosts
- `LogView(ContainerKey)` - Log viewer for a specific container with real-time streaming
- `ActionMenu(ContainerKey)` - Action menu popup for a specific container
- `SearchMode` - Search mode for filtering containers by name/ID

### Container Data Model (`core/types.rs::Container`)

The `Container` struct holds both static metadata and runtime statistics:

```rust
pub struct Container {
    pub id: String,                         // Truncated container ID (12 chars)
    pub name: String,                       // Container name
    pub state: ContainerState,              // Running, Paused, Exited, etc.
    pub health: Option<HealthStatus>,       // Healthy, Unhealthy, Starting (None if no health check)
    pub created: Option<DateTime<Utc>>,     // Container creation timestamp
    pub stats: ContainerStats,              // CPU, memory, network stats (updated in real-time)
    pub host_id: HostId,                    // Which Docker host this container belongs to
    pub dozzle_url: Option<String>,         // Dozzle URL for this container's host
}
```

**Container Identification:**
- Containers are uniquely identified by `ContainerKey { host_id, container_id }`
- This allows tracking the same container ID across different hosts
- Container IDs are truncated to 12 characters (Docker API accepts partial IDs)

### Docker Host Abstraction

The `DockerHost` struct (`docker/connection.rs`) encapsulates a Docker connection with its identifier and optional Dozzle URL:

```rust
pub struct DockerHost {
    pub host_id: HostId,
    pub docker: Docker,
    pub dozzle_url: Option<String>,
}
```

Host IDs are derived from the host specification:
- `"local"` → host_id = `"local"`
- `"ssh://user@host"` → host_id = `"user@host"`
- `"ssh://user@host:2222"` → host_id = `"user@host"` (port stripped)

**Dozzle Integration:**
- Dozzle URLs can be configured per-host in the config file
- Press 'o' in container list view to open Dozzle for the selected container
- Opens in browser at `{dozzle_url}/container/{container_id}` format
- Only works when not in an SSH session (detected via SSH_CLIENT/SSH_TTY/SSH_CONNECTION env vars)

### Configuration Loading

The `Config` struct (`cli/config.rs`) handles YAML configuration file loading:
- Searches multiple locations in priority order (see Configuration section above)
- Merges config file values with CLI arguments (CLI takes precedence)
- Uses `serde_yaml` for deserialization
- Uses `dirs` crate for home directory detection

**Host Configuration Format:**
The `HostConfig` struct contains:
- `host`: String - The Docker connection string (required)
- `dozzle`: Option<String> - URL to Dozzle instance (optional)
- Additional optional fields can be added in the future

All fields except `host` are optional and use `#[serde(skip_serializing_if = "Option::is_none")]`.

The merge logic:
- If CLI hosts are explicitly provided (not default), they override config file
- If CLI uses default (`--host local`) and config file has hosts, config file is used
- If both are empty/default, defaults to `local`
- CLI hosts are converted to `HostConfig` structs with `dozzle: None`

### Container Actions System

The application supports interactive container management through an action menu (`ui/action_menu.rs` and `docker/actions.rs`):

**Action Flow:**
1. User presses Right arrow (→) in container list
2. Action menu popup appears with available actions based on container state
3. User selects action with Up/Down arrows and presses Enter
4. Action is executed asynchronously via `execute_container_action()`
5. Docker events automatically update container state in UI

**Available Actions:**
- **Start**: Available for Exited, Created, Dead containers
- **Stop**: Available for Running, Paused containers (10-second timeout)
- **Restart**: Available for Running containers (10-second timeout)
- **Remove**: Available for any state except Restarting/Removing (forced removal)

**State-Based Availability:**
- Running → Stop, Restart, Remove
- Paused → Stop, Remove
- Exited/Created/Dead → Start, Remove
- Restarting/Removing → No actions available

**Implementation Details:**
- Actions spawn async tasks that don't block the UI
- Progress events sent back to main loop (InProgress, Success, Error)
- Container state updates happen automatically via Docker event stream
- Action menu closes immediately after execution for responsive UX

### Docker Connection

The `connect_docker()` function in `main.rs` handles four connection modes:
- `--host local`: Uses local Docker socket
- `--host ssh://user@host[:port]`: Connects via SSH (requires Bollard SSH feature)
- `--host tcp://host:port`: Connects via TCP to remote Docker daemon (unencrypted)
- `--host tls://host:port`: Connects via TLS to remote Docker daemon (encrypted, requires DOCKER_CERT_PATH)

Multiple `--host` arguments can be provided to monitor multiple Docker hosts simultaneously.

**Note:** TCP connections are unencrypted. Only use on trusted networks or with proper firewall rules. For encrypted connections, use TLS with certificates.

### Stats Calculation

Stats are calculated in `docker/stats.rs` with exponential smoothing applied:
- **CPU**: Delta between current and previous CPU usage, normalized by system CPU delta and CPU count
- **Memory**: Current usage divided by limit, expressed as percentage
- **Network**: Calculates TX/RX rates by tracking byte deltas over time
- **Smoothing**: Uses exponential moving average with alpha=0.3 to reduce noise and create smoother visualizations

### UI Rendering

The UI (`ui/render.rs`) uses pre-allocated styles to avoid per-frame allocations.

**Four View Modes:**
1. **Container List View** - Main table showing all containers
   - Dynamically shows/hides "Host" column (only shown when multiple hosts are connected)
   - Displays: ID, Name, Host (conditional), CPU%, Memory%, Net TX, Net RX, Status
   - Progress bars with percentage indicators for CPU and Memory
   - Network rates formatted as B/s, KB/s, MB/s, or GB/s
   - Search bar at bottom when in SearchMode (filters containers as you type)
2. **Log View** - Full-screen log streaming for selected container
   - Shows last 100 lines initially, then follows new logs
   - Timestamps displayed in yellow with bold formatting
   - Auto-scroll when at bottom, manual scroll preserves position
   - Displays "[AUTO]" or "[MANUAL]" indicator in title
3. **Action Menu** - Centered popup for container actions
   - Shows available actions based on container state
   - Actions: Start (stopped), Stop (running), Restart (running), Remove (any state)
   - Displays container name and host in title
   - Visual feedback with icons (▶ Start, ■ Stop, ↻ Restart, 🗑 Remove)
4. **Search Mode** - Filter containers by name/ID
   - Shows search input at bottom of container list
   - Filters containers in real-time as you type
   - Uses `tui-input` widget for text editing
   - Escape or Enter to exit search mode

**Color Coding for Metrics:**
- Green: 0-50%
- Yellow: 50.1-80%
- Red: >80%

**Sorting:** Containers can be sorted by multiple fields:
- Default sort: Uptime (newest first, descending)
- Sort fields: Uptime, Name, CPU, Memory
- Containers are always sorted by `host_id` first, then by the selected field within each host
- Press 's' to cycle through sort fields
- Press 'u'/'n'/'c'/'m' to sort by specific field (Uptime/Name/CPU/Memory)
- Pressing the same field key toggles sort direction (ascending/descending)
- Each field has a default direction: Name (ascending), Uptime/CPU/Memory (descending)
- Sort state is tracked in `SortState` with field and direction

**Container Filtering:**
- By default, only running containers are shown
- Press 'a' to toggle showing all containers (including stopped/exited containers)
- Filter state is tracked in `AppState::show_all_containers`
- Press '/' to enter search mode and filter by container name/ID
- Search filtering is case-insensitive and filters by both name and ID
- Filtered containers are automatically re-sorted after filtering

**Health Status:**
- Containers with health checks display their status: Healthy, Unhealthy, Starting
- Health status is parsed from Docker's health check information
- Status changes trigger UI updates via `ContainerHealthChanged` event

## CI/CD Workflows

The project uses `cargo-dist` for building and releasing binaries across multiple platforms.

### Release Workflow (`.github/workflows/release.yml`)
- Triggers on version tags (e.g., `v0.1.0`)
- Uses `cargo-dist` for cross-platform builds
- Builds for multiple platforms: Linux (x86_64, ARM64), macOS (x86_64, ARM64)
- Automatically creates GitHub releases with generated changelogs
- Produces installers, archives, and checksums
- Three main jobs:
  - `plan`: Determines what needs to be built
  - `build-local-artifacts`: Builds platform-specific binaries
  - `build-global-artifacts`: Creates installers and checksums
  - `host`: Uploads artifacts and creates GitHub release

### Other Workflows
- `.github/workflows/pr-build.yml` - Builds on pull requests
- `.github/workflows/docker-build.yml` - Builds Docker images for testing
- `.github/workflows/docker-release.yml` - Publishes Docker images on release
- `.github/workflows/test.yml` - Runs test suite

## Build Features

The project supports optional features to control binary size and dependencies:

### `self-update` Feature (enabled by default)
- Adds the `dtop update` subcommand for self-updating the binary
- Depends on `self_update` crate with rustls (adds ~1.9MB to binary size)
- **Included in**: Release binaries, cargo-dist builds, regular cargo builds
- **Excluded from**: Docker images (to minimize image size)

**Usage:**
```bash
# Build with self-update (default)
cargo build --release                        # Binary: ~3.8MB

# Build without self-update (smaller)
cargo build --release --no-default-features  # Binary: ~1.9MB
```

**Docker Configuration:**
The Dockerfile builds with `--no-default-features` to create minimal Docker images (~2.5MB vs ~4.7MB).
Since Docker containers are typically updated by pulling new images, the self-update feature isn't needed.
## Changelog Management

The project uses `git-cliff` for automated changelog generation based on conventional commits.

### Configuration

The changelog is configured in `cliff.toml` with the following settings:
- Follows conventional commit format (feat, fix, docs, chore, etc.)
- Groups commits by type (Features, Bug Fixes, Documentation, etc.)
- Filters out dependency update commits and release preparation commits
- Supports semantic versioning tags (v[0-9]+\.[0-9]+\.[0-9]+)
- Sorts commits within sections by oldest first

### Conventional Commit Format

Commits should follow this format:
```
<type>(<scope>): <description>

[optional body]

[optional footer]
```

**Common types:**
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `perf`: Performance improvements
- `refactor`: Code refactoring
- `style`: Code style changes
- `test`: Test changes
- `chore`: Maintenance tasks (builds, CI, etc.)

**Examples:**
```
feat(update): add self-update command
fix(stats): correct CPU calculation for multi-core systems
docs: update installation instructions
chore(deps): update rust crate bollard to v0.19.4
```

### Generating Changelogs

```bash
# View latest release changelog
git-cliff --latest

# View unreleased changes
git-cliff --unreleased

# Generate changelog for a version range
git-cliff --tag v0.3.0..v0.3.6

# Write full changelog to file
git-cliff -o CHANGELOG.md

# Generate changelog and update for next version
git-cliff --unreleased --tag v0.4.0 -o CHANGELOG.md
```

### Integration with Cargo

The project integrates git-cliff with both `cargo-release` and `cargo-dist`:

**cargo-dist integration (`dist-workspace.toml`):**
- `generate-changelog = true` - Automatically generates changelogs during releases
- `changelog-backend = "git-cliff"` - Uses git-cliff for changelog generation
- The GitHub release workflow will automatically include the generated changelog

**cargo-release integration (`Cargo.toml`):**
- `pre-release-replacements` - Automatically updates CHANGELOG.md during `cargo release`
- Adds new version entry when creating a release

**Release Workflow:**
1. Make changes and commit using conventional commit format
2. Run `cargo release <version>` to create a new release
3. CHANGELOG.md is automatically updated with the new version
4. Tag is created and pushed to GitHub
5. GitHub Actions (via cargo-dist) builds binaries and creates a release with the changelog

The `CHANGELOG.md` file is automatically maintained and should be committed to the repository.

## Key Dependencies

- **Tokio**: Async runtime for Docker API and event handling
- **Bollard**: Docker API client with SSH support (requires `ssh` feature)
- **Ratatui**: Terminal UI framework (v0.29)
- **Crossterm**: Cross-platform terminal manipulation (v0.29)
- **Clap**: CLI argument parsing with derive macros
- **Serde/Serde_yaml**: Configuration file deserialization
- **Dirs**: Cross-platform home directory detection
- **Chrono**: Date and time handling for log timestamps
- **Futures-util**: Stream utilities for async operations
- **Open**: Cross-platform URL opener for Dozzle integration
- **Ansi-to-tui**: ANSI escape code parsing for colored log output
- **Timeago**: Human-readable time formatting for container uptime
- **Tui-input**: Text input widget for search functionality

### Dev Dependencies
- **Insta**: Snapshot testing (use `cargo insta accept` to accept snapshots)
- **Mockall**: Mock generation for testing

## Performance Considerations

- UI refresh rate is throttled to 500ms to reduce CPU usage
- Event processing uses timeout-based throttling: waits for first event with timeout, then drains all pending
- Container stats streams run independently per container across all hosts
- Each host's container manager runs independently without blocking other hosts
- Keyboard polling is 200ms to balance responsiveness and CPU
- Styles are pre-allocated in `UiStyles::default()` to avoid allocations during rendering
- Container references (not clones) are used when building UI rows
- **Container sorting is throttled to once every 3 seconds** to avoid re-sorting on every render frame (stats updates are constant)
  - User-initiated sort changes (field selection, search, toggle filters) bypass throttle for immediate response
  - Container add/remove events also bypass throttle to maintain correctness
  - This reduces sorting from ~2/sec to ~0.33/sec during normal operation (~83% reduction)
- Exponential smoothing (alpha=0.3) reduces noise in stats without heavy computation
- Failed host connections are logged but don't prevent other hosts from being monitored
- Log streaming is only active when viewing a container's logs (stopped when exiting log view)
- Log text is formatted once when received and cached in `AppState::formatted_log_text` to avoid re-parsing ANSI codes on every frame
- ANSI parsing happens at log arrival time, not render time

## User Interactions

**Container List View:**
- `↑/↓` - Navigate between containers
- `Enter` - Open action menu for selected container
- `→/l` - View logs for selected container
- `q` or `Ctrl-C` - Quit application
- `o` - Open Dozzle for selected container (if configured and not in SSH session)
- `?` - Toggle help popup
- `/` - Enter search mode (filter containers)
- `s` - Cycle through sort fields (Uptime → Name → CPU → Memory → Uptime)
- `u` - Sort by Uptime (toggle direction if already sorting by Uptime)
- `n` - Sort by Name (toggle direction if already sorting by Name)
- `c` - Sort by CPU (toggle direction if already sorting by CPU)
- `m` - Sort by Memory (toggle direction if already sorting by Memory)
- `a` - Toggle showing all containers (including stopped containers)

**Log View:**
- `↑/↓` - Scroll through logs manually
- `←/h` or `Esc` - Return to container list
- `?` - Toggle help popup
- Auto-scroll behavior: Automatically scrolls to bottom when new logs arrive (unless manually scrolled up)

**Action Menu:**
- `↑/↓` - Navigate between available actions
- `Enter` - Execute selected action
- `Esc` - Cancel and return to container list
- Available actions depend on container state (e.g., running containers can be stopped/restarted)

**Search Mode:**
- Type to filter containers by name or ID (case-insensitive)
- `Backspace`/`Delete` - Edit search query
- `Enter` or `Esc` - Exit search mode
- `↑/↓` - Navigate filtered results while searching

## Testing Strategy

The codebase includes unit tests for:
- Stats calculation logic (`docker/stats.rs`): CPU percentage, memory percentage, edge cases
- UI color coding (`ui/render.rs`): Threshold boundaries for green/yellow/red
- Log parsing (`docker/logs.rs`): Timestamp parsing, message extraction, edge cases
- Config loading (`cli/config.rs`): YAML deserialization, CLI merging, host configurations
- UI snapshot tests (`ui/ui_tests.rs`): Visual regression testing using insta

Run tests with `cargo test` or `cargo insta test` for snapshot tests.

## Claude PR Review Guidelines

When reviewing pull requests for this repository, follow these guidelines:

- **Keep feedback concise and actionable** - Focus on what matters, not lengthy explanations
- **Prioritize issues by severity**:
  1. Security vulnerabilities (always report)
  2. Bugs and logic errors
  3. Performance issues
  4. API/interface issues
- **Skip minor nitpicks** - Don't comment on trivial style issues unless they significantly affect readability
- **Be direct** - Use short, clear sentences. Avoid filler phrases
- **Group related feedback** - Don't create separate comments for each small issue
- **Limit comments to 3-5 items** unless there are critical issues requiring more attention