Switch dashboard to ZMQ gossip data source
This commit is contained in:
parent
100056b790
commit
656cb5943b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
logs/
|
||||
3
AGENTS.md
Normal file
3
AGENTS.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Agent Guide
|
||||
|
||||
Agents working in this repo must follow the instructions in `CLAUDE.md`.
|
||||
620
CLAUDE.md
Normal file
620
CLAUDE.md
Normal file
@ -0,0 +1,620 @@
|
||||
# CM Dashboard - Infrastructure Monitoring TUI
|
||||
|
||||
## Overview
|
||||
A high-performance Rust-based TUI dashboard for monitoring CMTEC infrastructure. Built to replace Glance with a custom solution tailored for our specific monitoring needs and API integrations.
|
||||
|
||||
## Project Goals
|
||||
|
||||
### Core Objectives
|
||||
- **Real-time monitoring** of all infrastructure components
|
||||
- **Multi-host support** for cmbox, labbox, simonbox, steambox, srv01
|
||||
- **Performance-focused** with minimal resource usage
|
||||
- **Keyboard-driven interface** for power users
|
||||
- **Integration** with existing monitoring APIs (ports 6127, 6128, 6129)
|
||||
|
||||
### Key Features
|
||||
- **NVMe health monitoring** with wear prediction
|
||||
- **RAM optimization tracking** (tmpfs, zram, kernel metrics)
|
||||
- **Service resource monitoring** with sandboxed limits
|
||||
- **Backup status** with detailed metrics and history
|
||||
- **Email notification integration**
|
||||
- **Historical data tracking** and trend analysis
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Technology Stack
|
||||
- **Language**: Rust 🦀
|
||||
- **TUI Framework**: ratatui (modern tui-rs fork)
|
||||
- **Async Runtime**: tokio
|
||||
- **HTTP Client**: reqwest
|
||||
- **Serialization**: serde
|
||||
- **CLI**: clap
|
||||
- **Error Handling**: anyhow
|
||||
- **Time**: chrono
|
||||
|
||||
### Dependencies
|
||||
```toml
|
||||
[dependencies]
|
||||
ratatui = "0.24" # Modern TUI framework
|
||||
crossterm = "0.27" # Cross-platform terminal handling
|
||||
tokio = { version = "1.0", features = ["full"] } # Async runtime
|
||||
reqwest = { version = "0.11", features = ["json"] } # HTTP client
|
||||
serde = { version = "1.0", features = ["derive"] } # JSON parsing
|
||||
clap = { version = "4.0", features = ["derive"] } # CLI args
|
||||
anyhow = "1.0" # Error handling
|
||||
chrono = "0.4" # Time handling
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cm-dashboard/
|
||||
├── Cargo.toml
|
||||
├── README.md
|
||||
├── CLAUDE.md # This file
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point & CLI
|
||||
│ ├── app.rs # Main application state
|
||||
│ ├── ui/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── dashboard.rs # Main dashboard layout
|
||||
│ │ ├── nvme.rs # NVMe health widget
|
||||
│ │ ├── services.rs # Services status widget
|
||||
│ │ ├── memory.rs # RAM optimization widget
|
||||
│ │ ├── backup.rs # Backup status widget
|
||||
│ │ └── alerts.rs # Alerts/notifications widget
|
||||
│ ├── api/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── client.rs # HTTP client wrapper
|
||||
│ │ ├── smart.rs # Smart metrics API (port 6127)
|
||||
│ │ ├── service.rs # Service metrics API (port 6128)
|
||||
│ │ └── backup.rs # Backup metrics API (port 6129)
|
||||
│ ├── data/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── metrics.rs # Data structures
|
||||
│ │ ├── history.rs # Historical data storage
|
||||
│ │ └── config.rs # Host configuration
|
||||
│ └── config.rs # Application configuration
|
||||
├── config/
|
||||
│ ├── hosts.toml # Host definitions
|
||||
│ └── dashboard.toml # Dashboard layout config
|
||||
└── docs/
|
||||
├── API.md # API integration documentation
|
||||
└── WIDGETS.md # Widget development guide
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
### Existing CMTEC APIs
|
||||
1. **Smart Metrics API** (port 6127)
|
||||
- NVMe health status (wear, temperature, power-on hours)
|
||||
- Disk space information
|
||||
- SMART health indicators
|
||||
|
||||
2. **Service Metrics API** (port 6128)
|
||||
- Service status and resource usage
|
||||
- Memory consumption vs limits
|
||||
- Disk usage per service
|
||||
|
||||
3. **Backup Metrics API** (port 6129)
|
||||
- Backup status and history
|
||||
- Repository statistics
|
||||
- Service integration status
|
||||
|
||||
### Data Structures
|
||||
```rust
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct SmartMetrics {
|
||||
pub status: String,
|
||||
pub drives: Vec<DriveInfo>,
|
||||
pub summary: DriveSummary,
|
||||
pub issues: Vec<String>,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ServiceMetrics {
|
||||
pub summary: ServiceSummary,
|
||||
pub services: Vec<ServiceInfo>,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct BackupMetrics {
|
||||
pub overall_status: String,
|
||||
pub backup: BackupInfo,
|
||||
pub service: BackupServiceInfo,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
```
|
||||
|
||||
## Dashboard Layout Design
|
||||
|
||||
### Main Dashboard View
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 📊 CMTEC Infrastructure Dashboard srv01 │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 💾 NVMe Health │ 🐏 RAM Optimization │
|
||||
│ ┌─────────────────────────┐ │ ┌─────────────────────────────────────┐ │
|
||||
│ │ Wear: 4% (█░░░░░░░░░░) │ │ │ Physical: 2.4G/7.6G (32%) │ │
|
||||
│ │ Temp: 56°C │ │ │ zram: 64B/1.9G (64:1 compression) │ │
|
||||
│ │ Hours: 11419h (475d) │ │ │ tmpfs: /var/log 88K/512M │ │
|
||||
│ │ Status: ✅ PASSED │ │ │ Kernel: vm.dirty_ratio=5 │ │
|
||||
│ └─────────────────────────┘ │ └─────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 🔧 Services Status │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ✅ Gitea (256M/4G, 15G/100G) ✅ smart-metrics-api │ │
|
||||
│ │ ✅ Immich (1.2G/4G, 45G/500G) ✅ service-metrics-api │ │
|
||||
│ │ ✅ Vaultwarden (45M/1G, 512M/1G) ✅ backup-metrics-api │ │
|
||||
│ │ ✅ UniFi (234M/2G, 1.2G/5G) ✅ WordPress M2 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 📧 Recent Alerts │ 💾 Backup Status │
|
||||
│ 10:15 NVMe wear OK → 4% │ Last: ✅ Success (04:00) │
|
||||
│ 04:00 Backup completed successfully │ Duration: 45m 32s │
|
||||
│ Yesterday: Email notification test │ Size: 15.2GB → 4.1GB │
|
||||
│ │ Next: Tomorrow 04:00 │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
Keys: [h]osts [r]efresh [s]ettings [a]lerts [←→] navigate [q]uit
|
||||
```
|
||||
|
||||
### Multi-Host View
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 🖥️ CMTEC Host Overview │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Host │ NVMe Wear │ RAM Usage │ Services │ Last Alert │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ srv01 │ 4% ✅ │ 32% ✅ │ 8/8 ✅ │ 04:00 Backup OK │
|
||||
│ cmbox │ 12% ✅ │ 45% ✅ │ 3/3 ✅ │ Yesterday Email test │
|
||||
│ labbox │ 8% ✅ │ 28% ✅ │ 2/2 ✅ │ 2h ago NVMe temp OK │
|
||||
│ simonbox │ 15% ✅ │ 67% ⚠️ │ 4/4 ✅ │ Gaming session active │
|
||||
│ steambox │ 23% ✅ │ 78% ⚠️ │ 2/2 ✅ │ High RAM usage │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
Keys: [Enter] details [r]efresh [s]ort [f]ilter [q]uit
|
||||
```
|
||||
|
||||
## Development Phases
|
||||
|
||||
### Phase 1: Foundation (Week 1-2)
|
||||
- [x] Project setup with Cargo.toml
|
||||
- [ ] Basic TUI framework with ratatui
|
||||
- [ ] HTTP client for API connections
|
||||
- [ ] Data structures for metrics
|
||||
- [ ] Simple single-host dashboard
|
||||
|
||||
**Deliverables:**
|
||||
- Working TUI that connects to srv01
|
||||
- Real-time display of basic metrics
|
||||
- Keyboard navigation
|
||||
|
||||
### Phase 2: Core Features (Week 3-4)
|
||||
- [ ] All widget implementations
|
||||
- [ ] Multi-host configuration
|
||||
- [ ] Historical data storage
|
||||
- [ ] Alert system integration
|
||||
- [ ] Configuration management
|
||||
|
||||
**Deliverables:**
|
||||
- Full-featured dashboard
|
||||
- Multi-host monitoring
|
||||
- Historical trending
|
||||
- Configuration file support
|
||||
|
||||
### Phase 3: Advanced Features (Week 5-6)
|
||||
- [ ] Predictive analytics
|
||||
- [ ] Custom alert rules
|
||||
- [ ] Export capabilities
|
||||
- [ ] Performance optimizations
|
||||
- [ ] Error handling & resilience
|
||||
|
||||
**Deliverables:**
|
||||
- Production-ready dashboard
|
||||
- Advanced monitoring features
|
||||
- Comprehensive error handling
|
||||
- Performance benchmarks
|
||||
|
||||
### Phase 4: Polish & Documentation (Week 7-8)
|
||||
- [ ] Code documentation
|
||||
- [ ] User documentation
|
||||
- [ ] Installation scripts
|
||||
- [ ] Testing suite
|
||||
- [ ] Release preparation
|
||||
|
||||
**Deliverables:**
|
||||
- Complete documentation
|
||||
- Installation packages
|
||||
- Test coverage
|
||||
- Release v1.0
|
||||
|
||||
## Configuration
|
||||
|
||||
### Host Configuration (config/hosts.toml)
|
||||
```toml
|
||||
[hosts]
|
||||
|
||||
[hosts.srv01]
|
||||
name = "srv01"
|
||||
address = "192.168.30.100"
|
||||
smart_api = 6127
|
||||
service_api = 6128
|
||||
backup_api = 6129
|
||||
role = "server"
|
||||
|
||||
[hosts.cmbox]
|
||||
name = "cmbox"
|
||||
address = "192.168.30.101"
|
||||
smart_api = 6127
|
||||
service_api = 6128
|
||||
backup_api = 6129
|
||||
role = "workstation"
|
||||
|
||||
[hosts.labbox]
|
||||
name = "labbox"
|
||||
address = "192.168.30.102"
|
||||
smart_api = 6127
|
||||
service_api = 6128
|
||||
backup_api = 6129
|
||||
role = "lab"
|
||||
```
|
||||
|
||||
### Dashboard Configuration (config/dashboard.toml)
|
||||
```toml
|
||||
[dashboard]
|
||||
refresh_interval = 5 # seconds
|
||||
history_retention = 7 # days
|
||||
theme = "dark"
|
||||
|
||||
[widgets]
|
||||
nvme_wear_threshold = 70
|
||||
temperature_threshold = 70
|
||||
memory_warning_threshold = 80
|
||||
memory_critical_threshold = 90
|
||||
|
||||
[alerts]
|
||||
email_enabled = true
|
||||
sound_enabled = false
|
||||
desktop_notifications = true
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Real-time Monitoring
|
||||
- **Auto-refresh** configurable intervals (1-60 seconds)
|
||||
- **Async data fetching** from multiple hosts simultaneously
|
||||
- **Connection status** indicators for each host
|
||||
- **Graceful degradation** when hosts are unreachable
|
||||
|
||||
### Historical Tracking
|
||||
- **SQLite database** for local storage
|
||||
- **Trend analysis** for wear levels and resource usage
|
||||
- **Retention policies** configurable per metric type
|
||||
- **Export capabilities** (CSV, JSON)
|
||||
|
||||
### Alert System
|
||||
- **Threshold-based alerts** for all metrics
|
||||
- **Email integration** with existing notification system
|
||||
- **Alert acknowledgment** and history
|
||||
- **Custom alert rules** with logical operators
|
||||
|
||||
### Multi-Host Management
|
||||
- **Auto-discovery** of hosts on network
|
||||
- **Host grouping** by role (server, workstation, lab)
|
||||
- **Bulk operations** across multiple hosts
|
||||
- **Host-specific configurations**
|
||||
|
||||
## Performance Requirements
|
||||
|
||||
### Resource Usage
|
||||
- **Memory**: < 50MB runtime footprint
|
||||
- **CPU**: < 1% average CPU usage
|
||||
- **Network**: Minimal bandwidth (< 1KB/s per host)
|
||||
- **Startup**: < 2 seconds cold start
|
||||
|
||||
### Responsiveness
|
||||
- **UI updates**: 60 FPS smooth rendering
|
||||
- **Data refresh**: < 500ms API response handling
|
||||
- **Navigation**: Instant keyboard response
|
||||
- **Error recovery**: < 5 seconds reconnection
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Network Security
|
||||
- **Local network only** - no external connections
|
||||
- **Authentication** for API access if implemented
|
||||
- **Encrypted storage** for sensitive configuration
|
||||
- **Audit logging** for administrative actions
|
||||
|
||||
### Data Privacy
|
||||
- **Local storage** only - no cloud dependencies
|
||||
- **Configurable retention** for historical data
|
||||
- **Secure deletion** of expired data
|
||||
- **No sensitive data logging**
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- API client modules
|
||||
- Data parsing and validation
|
||||
- Configuration management
|
||||
- Alert logic
|
||||
|
||||
### Integration Tests
|
||||
- Multi-host connectivity
|
||||
- API error handling
|
||||
- Database operations
|
||||
- Alert delivery
|
||||
|
||||
### Performance Tests
|
||||
- Memory usage under load
|
||||
- Network timeout handling
|
||||
- Large dataset rendering
|
||||
- Extended runtime stability
|
||||
|
||||
## Deployment
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Development build
|
||||
cargo build --release
|
||||
|
||||
# Install from source
|
||||
cargo install --path .
|
||||
|
||||
# Future: Package distribution
|
||||
# Package for NixOS inclusion
|
||||
```
|
||||
|
||||
### Usage
|
||||
```bash
|
||||
# Start dashboard
|
||||
cm-dashboard
|
||||
|
||||
# Specify config
|
||||
cm-dashboard --config /path/to/config
|
||||
|
||||
# Single host mode
|
||||
cm-dashboard --host srv01
|
||||
|
||||
# Debug mode
|
||||
cm-dashboard --verbose
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Tasks
|
||||
- **Database cleanup** - automated retention policies
|
||||
- **Log rotation** - configurable log levels and retention
|
||||
- **Configuration validation** - startup configuration checks
|
||||
- **Performance monitoring** - built-in metrics for dashboard itself
|
||||
|
||||
### Updates
|
||||
- **Auto-update checks** - optional feature
|
||||
- **Configuration migration** - version compatibility
|
||||
- **API compatibility** - backwards compatibility with monitoring APIs
|
||||
- **Feature toggles** - enable/disable features without rebuild
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Proposed: ZMQ Metrics Agent Architecture
|
||||
|
||||
#### **Current Limitations of HTTP-based APIs**
|
||||
- **Performance overhead**: Python scripts with HTTP servers on each host
|
||||
- **Network complexity**: Multiple firewall ports (6127-6129) per host
|
||||
- **Polling inefficiency**: Manual refresh cycles instead of real-time streaming
|
||||
- **Scalability concerns**: Resource usage grows linearly with hosts
|
||||
|
||||
#### **Proposed: Rust ZMQ Gossip Network**
|
||||
|
||||
**Core Concept**: Replace HTTP polling with a peer-to-peer ZMQ gossip network where lightweight Rust agents stream metrics in real-time.
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ cmbox │<-->│ labbox │<-->│ srv01 │<-->│steambox │
|
||||
│ :6130 │ │ :6130 │ │ :6130 │ │ :6130 │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
^ ^ ^
|
||||
└────────────────────────────┼──────────────┘
|
||||
v
|
||||
┌─────────┐
|
||||
│simonbox │
|
||||
│ :6130 │
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
**Architecture Benefits**:
|
||||
- **No central router**: Peer-to-peer gossip eliminates single point of failure
|
||||
- **Self-healing**: Network automatically routes around failed hosts
|
||||
- **Real-time streaming**: Metrics pushed immediately on change
|
||||
- **Performance**: Rust agents ~10-100x faster than Python
|
||||
- **Simplified networking**: Single ZMQ port (6130) vs multiple HTTP ports
|
||||
- **Lower resource usage**: Minimal memory/CPU footprint per agent
|
||||
|
||||
#### **Implementation Plan**
|
||||
|
||||
**Phase 1: Agent Development**
|
||||
```rust
|
||||
// Lightweight agent on each host
|
||||
pub struct MetricsAgent {
|
||||
neighbors: Vec<String>, // ["srv01:6130", "cmbox:6130"]
|
||||
collectors: Vec<Box<dyn Collector>>, // SMART, Service, Backup
|
||||
gossip_interval: Duration, // How often to broadcast
|
||||
zmq_context: zmq::Context,
|
||||
}
|
||||
|
||||
// Message format for metrics
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MetricsMessage {
|
||||
hostname: String,
|
||||
agent_type: AgentType, // Smart, Service, Backup
|
||||
timestamp: u64,
|
||||
metrics: MetricsData,
|
||||
hop_count: u8, // Prevent infinite loops
|
||||
}
|
||||
```
|
||||
|
||||
**Phase 2: Dashboard Integration**
|
||||
- **ZMQ Subscriber**: Dashboard subscribes to gossip stream on srv01
|
||||
- **Real-time updates**: WebSocket connection to TUI for live streaming
|
||||
- **Historical storage**: Optional persistence layer for trending
|
||||
|
||||
**Phase 3: Migration Strategy**
|
||||
- **Parallel deployment**: Run ZMQ agents alongside existing HTTP APIs
|
||||
- **A/B comparison**: Validate metrics accuracy and performance
|
||||
- **Gradual cutover**: Switch dashboard to ZMQ, then remove HTTP services
|
||||
|
||||
#### **Configuration Integration**
|
||||
|
||||
**Agent Configuration** (per-host):
|
||||
```toml
|
||||
[metrics_agent]
|
||||
enabled = true
|
||||
port = 6130
|
||||
neighbors = ["srv01:6130", "cmbox:6130"] # Redundant connections
|
||||
role = "agent" # or "dashboard" for srv01
|
||||
|
||||
[collectors]
|
||||
smart_metrics = { enabled = true, interval_ms = 5000 }
|
||||
service_metrics = { enabled = true, interval_ms = 2000 } # srv01 only
|
||||
backup_metrics = { enabled = true, interval_ms = 30000 } # srv01 only
|
||||
```
|
||||
|
||||
**Dashboard Configuration** (updated):
|
||||
```toml
|
||||
[data_source]
|
||||
type = "zmq_gossip" # vs current "http_polling"
|
||||
listen_port = 6130
|
||||
buffer_size = 1000
|
||||
real_time_updates = true
|
||||
|
||||
[legacy_support]
|
||||
http_apis_enabled = true # For migration period
|
||||
fallback_to_http = true # If ZMQ unavailable
|
||||
```
|
||||
|
||||
#### **Performance Comparison**
|
||||
|
||||
| Metric | Current (HTTP) | Proposed (ZMQ) |
|
||||
|--------|---------------|----------------|
|
||||
| Collection latency | ~50ms | ~1ms |
|
||||
| Network overhead | HTTP headers + JSON | Binary ZMQ frames |
|
||||
| Resource per host | ~5MB (Python + HTTP) | ~1MB (Rust agent) |
|
||||
| Update frequency | 5s polling | Real-time push |
|
||||
| Network ports | 3 per host | 1 per host |
|
||||
| Failure recovery | Manual retry | Auto-reconnect |
|
||||
|
||||
#### **Development Roadmap**
|
||||
|
||||
**Week 1-2**: Basic ZMQ agent
|
||||
- Rust binary with ZMQ gossip protocol
|
||||
- SMART metrics collection
|
||||
- Configuration management
|
||||
|
||||
**Week 3-4**: Dashboard integration
|
||||
- ZMQ subscriber in cm-dashboard
|
||||
- Real-time TUI updates
|
||||
- Parallel HTTP/ZMQ operation
|
||||
|
||||
**Week 5-6**: Production readiness
|
||||
- Service/backup metrics support
|
||||
- Error handling and resilience
|
||||
- Performance benchmarking
|
||||
|
||||
**Week 7-8**: Migration and cleanup
|
||||
- Switch dashboard to ZMQ-only
|
||||
- Remove legacy HTTP APIs
|
||||
- Documentation and deployment
|
||||
|
||||
### Potential Features
|
||||
- **Plugin system** for custom widgets
|
||||
- **REST API** for external integrations
|
||||
- **Mobile companion app** for alerts
|
||||
- **Grafana integration** for advanced graphing
|
||||
- **Prometheus metrics export**
|
||||
- **Custom scripting** for automated responses
|
||||
- **Machine learning** for predictive analytics
|
||||
- **Clustering support** for high availability
|
||||
|
||||
### Integration Opportunities
|
||||
- **Home Assistant** integration
|
||||
- **Slack/Discord** notifications
|
||||
- **SNMP support** for network equipment
|
||||
- **Docker/Kubernetes** container monitoring
|
||||
- **Cloud metrics** integration (if needed)
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Technical Success
|
||||
- **Zero crashes** during normal operation
|
||||
- **Sub-second response** times for all operations
|
||||
- **99.9% uptime** for monitoring (excluding network issues)
|
||||
- **Minimal resource usage** as specified
|
||||
|
||||
### User Success
|
||||
- **Faster problem detection** compared to Glance
|
||||
- **Reduced time to resolution** for issues
|
||||
- **Improved infrastructure awareness**
|
||||
- **Enhanced operational efficiency**
|
||||
|
||||
---
|
||||
|
||||
## Development Log
|
||||
|
||||
### Project Initialization
|
||||
- Repository created: `/home/cm/projects/cm-dashboard`
|
||||
- Initial planning: TUI dashboard to replace Glance
|
||||
- Technology selected: Rust + ratatui
|
||||
- Architecture designed: Multi-host monitoring with existing API integration
|
||||
|
||||
### Current Status (HTTP-based)
|
||||
- **Functional TUI**: Basic dashboard rendering with ratatui
|
||||
- **HTTP API integration**: Connects to ports 6127, 6128, 6129
|
||||
- **Multi-host support**: Configurable host management
|
||||
- **Async architecture**: Tokio-based concurrent metrics fetching
|
||||
- **Configuration system**: TOML-based host and dashboard configuration
|
||||
|
||||
### Proposed Evolution: ZMQ Agent System
|
||||
|
||||
**Rationale for Change**: The current HTTP polling approach has fundamental limitations:
|
||||
1. **Latency**: 5-second refresh cycles miss rapid changes
|
||||
2. **Resource overhead**: Python HTTP servers consume unnecessary resources
|
||||
3. **Network complexity**: Multiple ports per host complicate firewall management
|
||||
4. **Scalability**: Linear resource growth with host count
|
||||
|
||||
**Solution**: Peer-to-peer ZMQ gossip network with Rust agents provides:
|
||||
- **Real-time streaming**: Sub-second metric propagation
|
||||
- **Fault tolerance**: Network self-heals around failed hosts
|
||||
- **Performance**: Native Rust speed vs interpreted Python
|
||||
- **Simplicity**: Single port per host, no central coordination
|
||||
|
||||
### ZMQ Agent Development Plan
|
||||
|
||||
**Component 1: cm-metrics-agent** (New Rust binary)
|
||||
```toml
|
||||
[package]
|
||||
name = "cm-metrics-agent"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
zmq = "0.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
smartmontools-rs = "0.1" # Or direct smartctl bindings
|
||||
```
|
||||
|
||||
**Component 2: Dashboard Integration** (Update cm-dashboard)
|
||||
- Add ZMQ subscriber mode alongside HTTP client
|
||||
- Implement real-time metric streaming
|
||||
- Provide migration path from HTTP to ZMQ
|
||||
|
||||
**Migration Strategy**:
|
||||
1. **Phase 1**: Deploy agents alongside existing APIs
|
||||
2. **Phase 2**: Switch dashboard to ZMQ mode
|
||||
3. **Phase 3**: Remove HTTP APIs from NixOS configurations
|
||||
|
||||
**Performance Targets**:
|
||||
- **Agent footprint**: < 2MB RAM, < 1% CPU
|
||||
- **Metric latency**: < 100ms propagation across network
|
||||
- **Network efficiency**: < 1KB/s per host steady state
|
||||
1693
Cargo.lock
generated
Normal file
1693
Cargo.lock
generated
Normal file
@ -0,0 +1,1693 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "cm-dashboard"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"zmq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dircpy"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a88521b0517f5f9d51d11925d8ab4523497dcf947073fa3231a311b63941131c"
|
||||
dependencies = [
|
||||
"jwalk",
|
||||
"log",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.7+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56"
|
||||
dependencies = [
|
||||
"crossbeam",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cassowary",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio 1.0.4",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-appender"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.7+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
|
||||
dependencies = [
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "zeromq-src"
|
||||
version = "0.2.6+4.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc120b771270365d5ed0dfb4baf1005f2243ae1ae83703265cb3504070f4160b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dircpy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmq"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd3091dd571fb84a9b3e5e5c6a807d186c411c812c8618786c3c30e5349234e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"zmq-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmq-sys"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8351dc72494b4d7f5652a681c33634063bbad58046c1689e75270908fdc864"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
"zeromq-src",
|
||||
]
|
||||
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "cm-dashboard"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ratatui = "0.24"
|
||||
crossterm = "0.27"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
toml = "0.8"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||
tracing-appender = "0.2"
|
||||
zmq = "0.10"
|
||||
136
README.md
136
README.md
@ -1,3 +1,135 @@
|
||||
# cm-dashboard
|
||||
# CM Dashboard
|
||||
|
||||
Linux TUI dashboard for host health overview
|
||||
CM Dashboard is a Rust-powered terminal UI for real-time monitoring of CMTEC infrastructure hosts. It aggregates SMART, service, and backup metrics from the existing CMTEC APIs and presents them in an efficient, keyboard-driven interface built with `ratatui`.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ CM Dashboard │
|
||||
├────────────────────────────┬────────────────────────────┬────────────────────┤
|
||||
│ NVMe Health │ Services │ Memory Optimization │
|
||||
│ Host: srv01 │ Host: srv01 │ Host: srv01 │
|
||||
│ Status: Healthy │ Services healthy: 5 │ Memory used: 2048 / │
|
||||
│ Drives healthy/warn/crit: │ Degraded: 1 Failed: 0 │ 4096 MiB (50.0%) │
|
||||
│ 4/0/0 │ CPU top service: 71.3% │ Last update: 12:34: │
|
||||
│ Capacity used: 512.0 / │ Total memory: 1536 / 2048 │ 56 │
|
||||
│ 2048.0 GiB │ MiB │ │
|
||||
├────────────────────────────┴────────────┬───────────────┴────────────────────┤
|
||||
│ Backups │ Alerts │
|
||||
│ Host: srv01 │ srv01: OK │
|
||||
│ Status: Healthy │ labbox: smart warning │
|
||||
│ Last success: 2024-02-01 03:12:45 │ │
|
||||
│ Snapshots: 17 • Size: 512.0 GiB │ │
|
||||
│ Pending jobs: 0 (enabled: true) │ │
|
||||
└──────────────────────────────┬───────────────────────────────────────────────┘
|
||||
│ Status │ │
|
||||
│ Active host: srv01 (1/3) │ History retention ≈ 3600s │
|
||||
│ Config: config/dashboard.toml│ Default host: labbox │
|
||||
└──────────────────────────────┴───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust toolchain 1.75+ (install via [`rustup`](https://rustup.rs))
|
||||
- Access to CMTEC monitoring APIs on ports 6127, 6128, and 6129 from the machine running the dashboard
|
||||
- Configuration files under `config/` describing hosts and dashboard preferences
|
||||
|
||||
## Installation
|
||||
|
||||
Clone the repository and build with Cargo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/cmtec/cm-dashboard.git
|
||||
cd cm-dashboard
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The optimized binary is available at `target/release/cm-dashboard`. To install into your Cargo bin directory:
|
||||
|
||||
```bash
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
On first launch, the dashboard will create `config/dashboard.toml` and `config/hosts.toml` automatically if they do not exist.
|
||||
|
||||
You can also generate starter configuration files manually with the built-in helper:
|
||||
|
||||
```bash
|
||||
cargo run -- init-config
|
||||
# or, once installed
|
||||
cm-dashboard init-config --dir ./config --force
|
||||
```
|
||||
|
||||
This produces `config/dashboard.toml` and `config/hosts.toml`. The primary dashboard config looks like:
|
||||
|
||||
```toml
|
||||
[hosts]
|
||||
default_host = "srv01"
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "srv01"
|
||||
enabled = true
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "labbox"
|
||||
enabled = true
|
||||
|
||||
[dashboard]
|
||||
tick_rate_ms = 250
|
||||
history_duration_minutes = 60
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "nvme"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "alerts"
|
||||
enabled = true
|
||||
|
||||
[data_source]
|
||||
kind = "zmq"
|
||||
|
||||
[data_source.zmq]
|
||||
endpoints = ["tcp://127.0.0.1:6130"]
|
||||
```
|
||||
|
||||
Adjust the host list and `data_source.zmq.endpoints` to match your CMTEC gossip network. If you prefer to manage hosts separately, edit the generated `hosts.toml` file.
|
||||
|
||||
## Features
|
||||
|
||||
- Rotating host selection with left/right arrows (`←`, `→`, `h`, `l`, `Tab`)
|
||||
- Live NVMe, service, memory, backup, and alert summaries per active host
|
||||
- Structured logging with `tracing` (`-v`/`-vv` to increase verbosity)
|
||||
- Help overlay (`?`) outlining keyboard shortcuts
|
||||
- Config-driven host discovery via `config/dashboard.toml`
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
cargo run -- --config config/dashboard.toml
|
||||
# specify a single host
|
||||
cargo run -- --host srv01
|
||||
# override ZMQ endpoints at runtime
|
||||
cargo run -- --zmq-endpoint tcp://srv01:6130,tcp://labbox:6130
|
||||
# increase logging verbosity
|
||||
cargo run -- -v
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
| `←` / `h` | Previous host |
|
||||
| `→` / `l` / `Tab` | Next host |
|
||||
| `?` | Toggle help overlay |
|
||||
| `r` | Update status message |
|
||||
| `q` / `Esc` | Quit |
|
||||
|
||||
## Development
|
||||
|
||||
- Format: `cargo fmt`
|
||||
- Check: `cargo check`
|
||||
- Run: `cargo run`
|
||||
|
||||
The dashboard subscribes to the CMTEC ZMQ gossip network (default `tcp://127.0.0.1:6130`). Received metrics are cached per host and retained in an in-memory ring buffer for future trend analysis.
|
||||
|
||||
44
config/dashboard.example.toml
Normal file
44
config/dashboard.example.toml
Normal file
@ -0,0 +1,44 @@
|
||||
# CM Dashboard configuration template
|
||||
|
||||
[hosts]
|
||||
# default_host = "srv01"
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "srv01"
|
||||
enabled = true
|
||||
# metadata = { rack = "R1" }
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "labbox"
|
||||
enabled = true
|
||||
|
||||
[dashboard]
|
||||
tick_rate_ms = 250
|
||||
history_duration_minutes = 60
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "nvme"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "services"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "backup"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "alerts"
|
||||
enabled = true
|
||||
|
||||
[data_source]
|
||||
kind = "zmq"
|
||||
|
||||
[data_source.zmq]
|
||||
endpoints = ["tcp://127.0.0.1:6130"]
|
||||
# subscribe = ""
|
||||
|
||||
[filesystem]
|
||||
# cache_dir = "/var/lib/cm-dashboard/cache"
|
||||
# history_dir = "/var/lib/cm-dashboard/history"
|
||||
39
config/dashboard.toml
Normal file
39
config/dashboard.toml
Normal file
@ -0,0 +1,39 @@
|
||||
# CM Dashboard configuration
|
||||
|
||||
[hosts]
|
||||
# default_host = "srv01"
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "srv01"
|
||||
base_url = "http://srv01.local"
|
||||
enabled = true
|
||||
# metadata = { rack = "R1" }
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "labbox"
|
||||
base_url = "http://labbox.local"
|
||||
enabled = true
|
||||
|
||||
[dashboard]
|
||||
tick_rate_ms = 250
|
||||
history_duration_minutes = 60
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "nvme"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "services"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "backup"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "alerts"
|
||||
enabled = true
|
||||
|
||||
[filesystem]
|
||||
# cache_dir = "/var/lib/cm-dashboard/cache"
|
||||
# history_dir = "/var/lib/cm-dashboard/history"
|
||||
12
config/hosts.example.toml
Normal file
12
config/hosts.example.toml
Normal file
@ -0,0 +1,12 @@
|
||||
# Hosts configuration template (optional if you want a separate hosts file)
|
||||
|
||||
[hosts]
|
||||
# default_host = "srv01"
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "srv01"
|
||||
enabled = true
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "labbox"
|
||||
enabled = true
|
||||
14
config/hosts.toml
Normal file
14
config/hosts.toml
Normal file
@ -0,0 +1,14 @@
|
||||
# Optional separate hosts configuration
|
||||
|
||||
[hosts]
|
||||
# default_host = "srv01"
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "srv01"
|
||||
base_url = "http://srv01.local"
|
||||
enabled = true
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "labbox"
|
||||
base_url = "http://labbox.local"
|
||||
enabled = true
|
||||
483
src/app.rs
Normal file
483
src/app.rs
Normal file
@ -0,0 +1,483 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
|
||||
use crate::config;
|
||||
use crate::data::config::{AppConfig, DataSourceKind, HostTarget, ZmqConfig};
|
||||
use crate::data::history::MetricsHistory;
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics};
|
||||
|
||||
/// Shared application settings derived from the CLI arguments.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppOptions {
|
||||
pub config: Option<PathBuf>,
|
||||
pub host: Option<String>,
|
||||
pub tick_rate: Duration,
|
||||
pub verbosity: u8,
|
||||
pub zmq_endpoints_override: Vec<String>,
|
||||
}
|
||||
|
||||
impl AppOptions {
|
||||
pub fn tick_rate(&self) -> Duration {
|
||||
self.tick_rate
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct HostRuntimeState {
|
||||
last_success: Option<DateTime<Utc>>,
|
||||
last_error: Option<String>,
|
||||
smart: Option<SmartMetrics>,
|
||||
services: Option<ServiceMetrics>,
|
||||
backup: Option<BackupMetrics>,
|
||||
}
|
||||
|
||||
/// Top-level application state container.
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
options: AppOptions,
|
||||
config: Option<AppConfig>,
|
||||
active_config_path: Option<PathBuf>,
|
||||
hosts: Vec<HostTarget>,
|
||||
history: MetricsHistory,
|
||||
host_states: HashMap<String, HostRuntimeState>,
|
||||
zmq_endpoints: Vec<String>,
|
||||
zmq_subscription: Option<String>,
|
||||
zmq_connected: bool,
|
||||
active_host_index: usize,
|
||||
show_help: bool,
|
||||
should_quit: bool,
|
||||
last_tick: Instant,
|
||||
tick_count: u64,
|
||||
status: String,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(options: AppOptions) -> Result<Self> {
|
||||
let (config, active_config_path) = Self::load_configuration(options.config.as_ref())?;
|
||||
|
||||
let hosts = Self::select_hosts(options.host.as_ref(), config.as_ref());
|
||||
let history_capacity = Self::history_capacity_hint(config.as_ref());
|
||||
let history = MetricsHistory::with_capacity(history_capacity);
|
||||
let host_states = hosts
|
||||
.iter()
|
||||
.map(|host| (host.name.clone(), HostRuntimeState::default()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let (mut zmq_endpoints, zmq_subscription) = Self::resolve_zmq_config(config.as_ref());
|
||||
if !options.zmq_endpoints_override.is_empty() {
|
||||
zmq_endpoints = options.zmq_endpoints_override.clone();
|
||||
}
|
||||
|
||||
let status = Self::build_initial_status(options.host.as_ref(), active_config_path.as_ref());
|
||||
|
||||
Ok(Self {
|
||||
options,
|
||||
config,
|
||||
active_config_path,
|
||||
hosts,
|
||||
history,
|
||||
host_states,
|
||||
zmq_endpoints,
|
||||
zmq_subscription,
|
||||
zmq_connected: false,
|
||||
active_host_index: 0,
|
||||
show_help: false,
|
||||
should_quit: false,
|
||||
last_tick: Instant::now(),
|
||||
tick_count: 0,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_tick(&mut self) {
|
||||
self.tick_count = self.tick_count.saturating_add(1);
|
||||
self.last_tick = Instant::now();
|
||||
let host_count = self.hosts.len();
|
||||
let retention = self.history.retention();
|
||||
self.status = format!(
|
||||
"Monitoring • hosts: {} • ticks: {} • refresh: {:?} • retention: {:?}",
|
||||
host_count, self.tick_count, self.options.tick_rate, retention
|
||||
);
|
||||
}
|
||||
|
||||
pub fn handle_key_event(&mut self, key: KeyEvent) {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => {
|
||||
self.should_quit = true;
|
||||
self.status = "Exiting…".to_string();
|
||||
}
|
||||
KeyCode::Char('r') | KeyCode::Char('R') => {
|
||||
self.status = "Manual refresh requested".to_string();
|
||||
}
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
self.select_previous_host();
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('l') | KeyCode::Tab => {
|
||||
self.select_next_host();
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
self.show_help = !self.show_help;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_quit(&self) -> bool {
|
||||
self.should_quit
|
||||
}
|
||||
|
||||
pub fn status_text(&self) -> &str {
|
||||
&self.status
|
||||
}
|
||||
|
||||
pub fn zmq_connected(&self) -> bool {
|
||||
self.zmq_connected
|
||||
}
|
||||
|
||||
pub fn tick_rate(&self) -> Duration {
|
||||
self.options.tick_rate()
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Option<&AppConfig> {
|
||||
self.config.as_ref()
|
||||
}
|
||||
|
||||
pub fn active_config_path(&self) -> Option<&PathBuf> {
|
||||
self.active_config_path.as_ref()
|
||||
}
|
||||
|
||||
pub fn hosts(&self) -> &[HostTarget] {
|
||||
&self.hosts
|
||||
}
|
||||
|
||||
pub fn active_host_info(&self) -> Option<(usize, &HostTarget)> {
|
||||
if self.hosts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let index = self
|
||||
.active_host_index
|
||||
.min(self.hosts.len().saturating_sub(1));
|
||||
Some((index, &self.hosts[index]))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn history(&self) -> &MetricsHistory {
|
||||
&self.history
|
||||
}
|
||||
|
||||
pub fn host_display_data(&self) -> Vec<HostDisplayData> {
|
||||
self.hosts
|
||||
.iter()
|
||||
.filter_map(|host| {
|
||||
self.host_states
|
||||
.get(&host.name)
|
||||
.map(|state| HostDisplayData {
|
||||
name: host.name.clone(),
|
||||
last_success: state.last_success.clone(),
|
||||
last_error: state.last_error.clone(),
|
||||
smart: state.smart.clone(),
|
||||
services: state.services.clone(),
|
||||
backup: state.backup.clone(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn active_host_display(&self) -> Option<HostDisplayData> {
|
||||
self.active_host_info().and_then(|(_, host)| {
|
||||
self.host_states
|
||||
.get(&host.name)
|
||||
.map(|state| HostDisplayData {
|
||||
name: host.name.clone(),
|
||||
last_success: state.last_success.clone(),
|
||||
last_error: state.last_error.clone(),
|
||||
smart: state.smart.clone(),
|
||||
services: state.services.clone(),
|
||||
backup: state.backup.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn zmq_context(&self) -> Option<ZmqContext> {
|
||||
if self.zmq_endpoints.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ZmqContext::new(
|
||||
self.zmq_endpoints.clone(),
|
||||
self.zmq_subscription.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn handle_app_event(&mut self, event: AppEvent) {
|
||||
match event {
|
||||
AppEvent::MetricsUpdated {
|
||||
host,
|
||||
smart,
|
||||
services,
|
||||
backup,
|
||||
timestamp,
|
||||
} => {
|
||||
self.zmq_connected = true;
|
||||
self.ensure_host_entry(&host);
|
||||
let state = self.host_states.entry(host.clone()).or_default();
|
||||
state.last_success = Some(timestamp);
|
||||
state.last_error = None;
|
||||
|
||||
if let Some(mut smart_metrics) = smart {
|
||||
if smart_metrics.timestamp != timestamp {
|
||||
smart_metrics.timestamp = timestamp;
|
||||
}
|
||||
let snapshot = smart_metrics.clone();
|
||||
self.history.record_smart(smart_metrics);
|
||||
state.smart = Some(snapshot);
|
||||
}
|
||||
|
||||
if let Some(mut service_metrics) = services {
|
||||
if service_metrics.timestamp != timestamp {
|
||||
service_metrics.timestamp = timestamp;
|
||||
}
|
||||
let snapshot = service_metrics.clone();
|
||||
self.history.record_services(service_metrics);
|
||||
state.services = Some(snapshot);
|
||||
}
|
||||
|
||||
if let Some(mut backup_metrics) = backup {
|
||||
if backup_metrics.timestamp != timestamp {
|
||||
backup_metrics.timestamp = timestamp;
|
||||
}
|
||||
let snapshot = backup_metrics.clone();
|
||||
self.history.record_backup(backup_metrics);
|
||||
state.backup = Some(snapshot);
|
||||
}
|
||||
|
||||
self.status = format!(
|
||||
"Metrics update • host: {} • at {}",
|
||||
host,
|
||||
timestamp.format("%H:%M:%S")
|
||||
);
|
||||
}
|
||||
AppEvent::MetricsFailed {
|
||||
host,
|
||||
error,
|
||||
timestamp,
|
||||
} => {
|
||||
self.zmq_connected = false;
|
||||
self.ensure_host_entry(&host);
|
||||
let state = self.host_states.entry(host.clone()).or_default();
|
||||
state.last_error = Some(format!("{} at {}", error, timestamp.format("%H:%M:%S")));
|
||||
|
||||
self.status = format!("Fetch failed • host: {} • {}", host, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help_visible(&self) -> bool {
|
||||
self.show_help
|
||||
}
|
||||
|
||||
fn ensure_host_entry(&mut self, host: &str) {
|
||||
if !self.host_states.contains_key(host) {
|
||||
self.host_states
|
||||
.insert(host.to_string(), HostRuntimeState::default());
|
||||
}
|
||||
|
||||
if self.hosts.iter().any(|entry| entry.name == host) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.hosts.push(HostTarget::from_name(host.to_string()));
|
||||
if self.hosts.len() == 1 {
|
||||
self.active_host_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn load_configuration(path: Option<&PathBuf>) -> Result<(Option<AppConfig>, Option<PathBuf>)> {
|
||||
if let Some(explicit) = path {
|
||||
let config = config::load_from_path(explicit)?;
|
||||
return Ok((Some(config), Some(explicit.clone())));
|
||||
}
|
||||
|
||||
let default_path = PathBuf::from("config/dashboard.toml");
|
||||
if default_path.exists() {
|
||||
let config = config::load_from_path(&default_path)?;
|
||||
return Ok((Some(config), Some(default_path)));
|
||||
}
|
||||
|
||||
Ok((None, None))
|
||||
}
|
||||
|
||||
fn build_initial_status(host: Option<&String>, config_path: Option<&PathBuf>) -> String {
|
||||
match (host, config_path) {
|
||||
(Some(host), Some(path)) => {
|
||||
format!("Ready • host: {} • config: {}", host, path.display())
|
||||
}
|
||||
(Some(host), None) => format!("Ready • host: {}", host),
|
||||
(None, Some(path)) => format!("Ready • config: {}", path.display()),
|
||||
(None, None) => "Ready • no host selected".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_hosts(host: Option<&String>, config: Option<&AppConfig>) -> Vec<HostTarget> {
|
||||
let mut targets = Vec::new();
|
||||
|
||||
let Some(config) = config else {
|
||||
return targets;
|
||||
};
|
||||
|
||||
let host_filter = host.map(|value| value.to_lowercase());
|
||||
|
||||
for entry in &config.hosts.hosts {
|
||||
if !entry.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(filter) = &host_filter {
|
||||
if entry.name.to_lowercase() != *filter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
targets.push(entry.clone());
|
||||
}
|
||||
|
||||
if targets.is_empty() {
|
||||
if let Some(default_host) = &config.hosts.default_host {
|
||||
if host_filter.is_none() {
|
||||
if let Some(entry) = config.hosts.hosts.iter().find(|candidate| {
|
||||
candidate.enabled && candidate.name.eq_ignore_ascii_case(default_host)
|
||||
}) {
|
||||
targets.push(entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targets
|
||||
}
|
||||
|
||||
fn history_capacity_hint(config: Option<&AppConfig>) -> usize {
|
||||
const DEFAULT_CAPACITY: usize = 120;
|
||||
const SAMPLE_SECONDS: u64 = 30;
|
||||
|
||||
let Some(config) = config else {
|
||||
return DEFAULT_CAPACITY;
|
||||
};
|
||||
|
||||
let minutes = config.dashboard.history_duration_minutes.max(1);
|
||||
let total_seconds = minutes.saturating_mul(60);
|
||||
let samples = total_seconds / SAMPLE_SECONDS;
|
||||
usize::try_from(samples.max(1)).unwrap_or(DEFAULT_CAPACITY)
|
||||
}
|
||||
|
||||
fn select_previous_host(&mut self) {
|
||||
if self.hosts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active_host_index = if self.active_host_index == 0 {
|
||||
self.hosts.len().saturating_sub(1)
|
||||
} else {
|
||||
self.active_host_index - 1
|
||||
};
|
||||
self.status = format!(
|
||||
"Active host switched to {} ({}/{})",
|
||||
self.hosts[self.active_host_index].name,
|
||||
self.active_host_index + 1,
|
||||
self.hosts.len()
|
||||
);
|
||||
}
|
||||
|
||||
fn select_next_host(&mut self) {
|
||||
if self.hosts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active_host_index = (self.active_host_index + 1) % self.hosts.len();
|
||||
self.status = format!(
|
||||
"Active host switched to {} ({}/{})",
|
||||
self.hosts[self.active_host_index].name,
|
||||
self.active_host_index + 1,
|
||||
self.hosts.len()
|
||||
);
|
||||
}
|
||||
|
||||
fn resolve_zmq_config(config: Option<&AppConfig>) -> (Vec<String>, Option<String>) {
|
||||
let default = ZmqConfig::default();
|
||||
let zmq_config = config
|
||||
.and_then(|cfg| {
|
||||
if cfg.data_source.kind == DataSourceKind::Zmq {
|
||||
Some(cfg.data_source.zmq.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(default);
|
||||
|
||||
let endpoints = if zmq_config.endpoints.is_empty() {
|
||||
ZmqConfig::default().endpoints
|
||||
} else {
|
||||
zmq_config.endpoints.clone()
|
||||
};
|
||||
|
||||
(endpoints, zmq_config.subscribe.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HostDisplayData {
|
||||
pub name: String,
|
||||
pub last_success: Option<DateTime<Utc>>,
|
||||
pub last_error: Option<String>,
|
||||
pub smart: Option<SmartMetrics>,
|
||||
pub services: Option<ServiceMetrics>,
|
||||
pub backup: Option<BackupMetrics>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZmqContext {
|
||||
endpoints: Vec<String>,
|
||||
subscription: Option<String>,
|
||||
}
|
||||
|
||||
impl ZmqContext {
|
||||
pub fn new(endpoints: Vec<String>, subscription: Option<String>) -> Self {
|
||||
Self {
|
||||
endpoints,
|
||||
subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endpoints(&self) -> &[String] {
|
||||
&self.endpoints
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Option<&str> {
|
||||
self.subscription.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppEvent {
|
||||
MetricsUpdated {
|
||||
host: String,
|
||||
smart: Option<SmartMetrics>,
|
||||
services: Option<ServiceMetrics>,
|
||||
backup: Option<BackupMetrics>,
|
||||
timestamp: DateTime<Utc>,
|
||||
},
|
||||
MetricsFailed {
|
||||
host: String,
|
||||
error: String,
|
||||
timestamp: DateTime<Utc>,
|
||||
},
|
||||
}
|
||||
19
src/config.rs
Normal file
19
src/config.rs
Normal file
@ -0,0 +1,19 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::data::config::AppConfig;
|
||||
|
||||
/// Load application configuration from a TOML file.
|
||||
pub fn load_from_path(path: &Path) -> Result<AppConfig> {
|
||||
let raw = fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read configuration file at {}", path.display()))?;
|
||||
|
||||
let config = toml::from_str::<AppConfig>(&raw)
|
||||
.with_context(|| format!("failed to parse configuration file {}", path.display()))?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
138
src/data/config.rs
Normal file
138
src/data/config.rs
Normal file
@ -0,0 +1,138 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HostsConfig {
|
||||
pub default_host: Option<String>,
|
||||
#[serde(default)]
|
||||
pub hosts: Vec<HostTarget>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HostTarget {
|
||||
pub name: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl HostTarget {
|
||||
pub fn from_name(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
enabled: true,
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DashboardConfig {
|
||||
#[serde(default = "default_tick_rate_ms")]
|
||||
pub tick_rate_ms: u64,
|
||||
#[serde(default)]
|
||||
pub history_duration_minutes: u64,
|
||||
#[serde(default)]
|
||||
pub widgets: Vec<WidgetConfig>,
|
||||
}
|
||||
|
||||
impl Default for DashboardConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tick_rate_ms: default_tick_rate_ms(),
|
||||
history_duration_minutes: 60,
|
||||
widgets: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct WidgetConfig {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub options: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AppFilesystem {
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
pub history_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub hosts: HostsConfig,
|
||||
#[serde(default)]
|
||||
pub dashboard: DashboardConfig,
|
||||
#[serde(default = "default_data_source_config")]
|
||||
pub data_source: DataSourceConfig,
|
||||
#[serde(default)]
|
||||
pub filesystem: Option<AppFilesystem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DataSourceConfig {
|
||||
#[serde(default = "default_data_source_kind")]
|
||||
pub kind: DataSourceKind,
|
||||
#[serde(default)]
|
||||
pub zmq: ZmqConfig,
|
||||
}
|
||||
|
||||
impl Default for DataSourceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kind: DataSourceKind::Zmq,
|
||||
zmq: ZmqConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DataSourceKind {
|
||||
Zmq,
|
||||
}
|
||||
|
||||
fn default_data_source_kind() -> DataSourceKind {
|
||||
DataSourceKind::Zmq
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ZmqConfig {
|
||||
#[serde(default = "default_zmq_endpoints")]
|
||||
pub endpoints: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub subscribe: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ZmqConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
endpoints: default_zmq_endpoints(),
|
||||
subscribe: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
const fn default_tick_rate_ms() -> u64 {
|
||||
500
|
||||
}
|
||||
|
||||
fn default_data_source_config() -> DataSourceConfig {
|
||||
DataSourceConfig::default()
|
||||
}
|
||||
|
||||
fn default_zmq_endpoints() -> Vec<String> {
|
||||
vec!["tcp://127.0.0.1:6130".to_string()]
|
||||
}
|
||||
54
src/data/history.rs
Normal file
54
src/data/history.rs
Normal file
@ -0,0 +1,54 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics};
|
||||
|
||||
/// Ring buffer for retaining recent samples for trend analysis.
|
||||
#[derive(Debug)]
|
||||
pub struct MetricsHistory {
|
||||
capacity: usize,
|
||||
smart: VecDeque<(DateTime<Utc>, SmartMetrics)>,
|
||||
services: VecDeque<(DateTime<Utc>, ServiceMetrics)>,
|
||||
backups: VecDeque<(DateTime<Utc>, BackupMetrics)>,
|
||||
}
|
||||
|
||||
impl MetricsHistory {
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
capacity,
|
||||
smart: VecDeque::with_capacity(capacity),
|
||||
services: VecDeque::with_capacity(capacity),
|
||||
backups: VecDeque::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_smart(&mut self, metrics: SmartMetrics) {
|
||||
let entry = (Utc::now(), metrics);
|
||||
Self::push_with_limit(&mut self.smart, entry, self.capacity);
|
||||
}
|
||||
|
||||
pub fn record_services(&mut self, metrics: ServiceMetrics) {
|
||||
let entry = (Utc::now(), metrics);
|
||||
Self::push_with_limit(&mut self.services, entry, self.capacity);
|
||||
}
|
||||
|
||||
pub fn record_backup(&mut self, metrics: BackupMetrics) {
|
||||
let entry = (Utc::now(), metrics);
|
||||
Self::push_with_limit(&mut self.backups, entry, self.capacity);
|
||||
}
|
||||
|
||||
pub fn retention(&self) -> Duration {
|
||||
Duration::from_secs((self.capacity as u64) * 30)
|
||||
}
|
||||
|
||||
fn push_with_limit<T>(deque: &mut VecDeque<T>, item: T, capacity: usize) {
|
||||
if deque.len() == capacity {
|
||||
deque.pop_front();
|
||||
}
|
||||
deque.push_back(item);
|
||||
}
|
||||
}
|
||||
96
src/data/metrics.rs
Normal file
96
src/data/metrics.rs
Normal file
@ -0,0 +1,96 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SmartMetrics {
|
||||
pub status: String,
|
||||
pub drives: Vec<DriveInfo>,
|
||||
pub summary: DriveSummary,
|
||||
pub issues: Vec<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DriveInfo {
|
||||
pub name: String,
|
||||
pub temperature_c: f32,
|
||||
pub wear_level: f32,
|
||||
pub power_on_hours: u64,
|
||||
pub available_spare: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DriveSummary {
|
||||
pub healthy: usize,
|
||||
pub warning: usize,
|
||||
pub critical: usize,
|
||||
pub capacity_total_gb: f32,
|
||||
pub capacity_used_gb: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceMetrics {
|
||||
pub summary: ServiceSummary,
|
||||
pub services: Vec<ServiceInfo>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceSummary {
|
||||
pub healthy: usize,
|
||||
pub degraded: usize,
|
||||
pub failed: usize,
|
||||
pub memory_used_mb: f32,
|
||||
pub memory_quota_mb: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceInfo {
|
||||
pub name: String,
|
||||
pub status: ServiceStatus,
|
||||
pub memory_used_mb: f32,
|
||||
pub memory_quota_mb: f32,
|
||||
pub cpu_percent: f32,
|
||||
pub sandbox_limit: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServiceStatus {
|
||||
Running,
|
||||
Degraded,
|
||||
Restarting,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupMetrics {
|
||||
pub overall_status: BackupStatus,
|
||||
pub backup: BackupInfo,
|
||||
pub service: BackupServiceInfo,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupInfo {
|
||||
pub last_success: Option<DateTime<Utc>>,
|
||||
pub last_failure: Option<DateTime<Utc>>,
|
||||
pub size_gb: f32,
|
||||
pub snapshot_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackupServiceInfo {
|
||||
pub enabled: bool,
|
||||
pub pending_jobs: u32,
|
||||
pub last_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum BackupStatus {
|
||||
Healthy,
|
||||
Warning,
|
||||
Failed,
|
||||
Unknown,
|
||||
}
|
||||
3
src/data/mod.rs
Normal file
3
src/data/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod config;
|
||||
pub mod history;
|
||||
pub mod metrics;
|
||||
492
src/main.rs
Normal file
492
src/main.rs
Normal file
@ -0,0 +1,492 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod data;
|
||||
mod ui;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Stdout};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use clap::{ArgAction, Parser, Subcommand};
|
||||
use crossterm::event::{self, Event};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use crossterm::{execute, terminal};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::mpsc::{
|
||||
error::TryRecvError, unbounded_channel, UnboundedReceiver, UnboundedSender,
|
||||
};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::{debug, warn};
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use zmq::{Context as NativeZmqContext, Message as NativeZmqMessage};
|
||||
|
||||
use crate::app::{App, AppEvent, AppOptions, ZmqContext};
|
||||
|
||||
static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "cm-dashboard",
|
||||
version,
|
||||
about = "Infrastructure monitoring TUI for CMTEC"
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
/// Optional path to configuration TOML file
|
||||
#[arg(long, value_name = "FILE")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Limit dashboard to a single host
|
||||
#[arg(short = 'H', long, value_name = "HOST")]
|
||||
host: Option<String>,
|
||||
|
||||
/// Interval (ms) to refresh dashboard when idle
|
||||
#[arg(long, default_value_t = 250)]
|
||||
tick_rate: u64,
|
||||
|
||||
/// Increase logging verbosity (-v, -vv)
|
||||
#[arg(short, long, action = ArgAction::Count)]
|
||||
verbose: u8,
|
||||
|
||||
/// Override ZMQ endpoints (comma-separated)
|
||||
#[arg(long, value_delimiter = ',', value_name = "ENDPOINT")]
|
||||
zmq_endpoint: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Generate default configuration files
|
||||
InitConfig {
|
||||
#[arg(long, value_name = "DIR", default_value = "config")]
|
||||
dir: PathBuf,
|
||||
/// Overwrite existing files if they already exist
|
||||
#[arg(long, action = ArgAction::SetTrue)]
|
||||
force: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if let Some(Command::InitConfig { dir, force }) = cli.command.as_ref() {
|
||||
init_tracing(cli.verbose)?;
|
||||
generate_config_templates(dir, *force)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ensure_default_config(&cli)?;
|
||||
|
||||
let options = AppOptions {
|
||||
config: cli.config,
|
||||
host: cli.host,
|
||||
tick_rate: Duration::from_millis(cli.tick_rate.max(16)),
|
||||
verbosity: cli.verbose,
|
||||
zmq_endpoints_override: cli.zmq_endpoint,
|
||||
};
|
||||
|
||||
init_tracing(options.verbosity)?;
|
||||
|
||||
let mut app = App::new(options)?;
|
||||
let (event_tx, mut event_rx) = unbounded_channel();
|
||||
|
||||
if let Some(context) = app.zmq_context() {
|
||||
spawn_metrics_task(context, event_tx.clone());
|
||||
}
|
||||
|
||||
let mut terminal = setup_terminal()?;
|
||||
let result = run_app(&mut terminal, &mut app, &mut event_rx);
|
||||
teardown_terminal(terminal)?;
|
||||
result
|
||||
}
|
||||
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, terminal::EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn teardown_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), terminal::LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app(
|
||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
app: &mut App,
|
||||
event_rx: &mut UnboundedReceiver<AppEvent>,
|
||||
) -> Result<()> {
|
||||
let tick_rate = app.tick_rate();
|
||||
|
||||
while !app.should_quit() {
|
||||
drain_app_events(app, event_rx);
|
||||
terminal.draw(|frame| ui::render(frame, app))?;
|
||||
|
||||
if event::poll(tick_rate)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
app.handle_key_event(key);
|
||||
}
|
||||
} else {
|
||||
app.on_tick();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn drain_app_events(app: &mut App, receiver: &mut UnboundedReceiver<AppEvent>) {
|
||||
loop {
|
||||
match receiver.try_recv() {
|
||||
Ok(event) => app.handle_app_event(event),
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tracing(verbosity: u8) -> Result<()> {
|
||||
let level = match verbosity {
|
||||
0 => "warn",
|
||||
1 => "info",
|
||||
2 => "debug",
|
||||
_ => "trace",
|
||||
};
|
||||
|
||||
let env_filter = std::env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|value| EnvFilter::try_new(value).ok())
|
||||
.unwrap_or_else(|| EnvFilter::new(level));
|
||||
|
||||
let writer = prepare_log_writer()?;
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(env_filter)
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.with_writer(writer)
|
||||
.compact()
|
||||
.try_init()
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_log_writer() -> Result<tracing_appender::non_blocking::NonBlocking> {
|
||||
let logs_dir = Path::new("logs");
|
||||
if !logs_dir.exists() {
|
||||
fs::create_dir_all(logs_dir).with_context(|| {
|
||||
format!("failed to create logs directory at {}", logs_dir.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let file_appender = tracing_appender::rolling::never(logs_dir, "cm-dashboard.log");
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
LOG_GUARD.get_or_init(|| guard);
|
||||
Ok(non_blocking)
|
||||
}
|
||||
|
||||
fn spawn_metrics_task(context: ZmqContext, sender: UnboundedSender<AppEvent>) {
|
||||
tokio::spawn(async move {
|
||||
match spawn_blocking(move || metrics_blocking_loop(context, sender)).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(error)) => warn!(%error, "ZMQ metrics worker exited with error"),
|
||||
Err(join_error) => warn!(%join_error, "ZMQ metrics worker panicked"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn metrics_blocking_loop(context: ZmqContext, sender: UnboundedSender<AppEvent>) -> Result<()> {
|
||||
let zmq_context = NativeZmqContext::new();
|
||||
let socket = zmq_context
|
||||
.socket(zmq::SUB)
|
||||
.context("failed to create ZMQ SUB socket")?;
|
||||
|
||||
for endpoint in context.endpoints() {
|
||||
debug!(%endpoint, "connecting to ZMQ endpoint");
|
||||
socket
|
||||
.connect(endpoint)
|
||||
.with_context(|| format!("failed to connect to {endpoint}"))?;
|
||||
}
|
||||
|
||||
if let Some(prefix) = context.subscription() {
|
||||
socket
|
||||
.set_subscribe(prefix.as_bytes())
|
||||
.context("failed to set ZMQ subscription")?;
|
||||
} else {
|
||||
socket
|
||||
.set_subscribe(b"")
|
||||
.context("failed to subscribe to all ZMQ topics")?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match socket.recv_msg(0) {
|
||||
Ok(message) => {
|
||||
if let Err(error) = handle_zmq_message(&message, &sender) {
|
||||
warn!(%error, "failed to handle ZMQ message");
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(%error, "ZMQ receive error");
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_zmq_message(
|
||||
message: &NativeZmqMessage,
|
||||
sender: &UnboundedSender<AppEvent>,
|
||||
) -> Result<()> {
|
||||
let bytes = message.to_vec();
|
||||
|
||||
let envelope: MetricsEnvelope =
|
||||
serde_json::from_slice(&bytes).with_context(|| "failed to deserialize metrics envelope")?;
|
||||
let timestamp = Utc
|
||||
.timestamp_opt(envelope.timestamp as i64, 0)
|
||||
.single()
|
||||
.unwrap_or_else(|| Utc::now());
|
||||
|
||||
let host = envelope.hostname.clone();
|
||||
|
||||
let mut payload = envelope.metrics;
|
||||
if let Some(obj) = payload.as_object_mut() {
|
||||
obj.entry("timestamp")
|
||||
.or_insert_with(|| Value::String(timestamp.to_rfc3339()));
|
||||
}
|
||||
|
||||
match envelope.agent_type {
|
||||
AgentType::Smart => match serde_json::from_value::<SmartMetrics>(payload.clone()) {
|
||||
Ok(metrics) => {
|
||||
let _ = sender.send(AppEvent::MetricsUpdated {
|
||||
host,
|
||||
smart: Some(metrics),
|
||||
services: None,
|
||||
backup: None,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(%error, "failed to parse smart metrics");
|
||||
let _ = sender.send(AppEvent::MetricsFailed {
|
||||
host,
|
||||
error: format!("smart metrics parse error: {error:#}"),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
},
|
||||
AgentType::Service => match serde_json::from_value::<ServiceMetrics>(payload.clone()) {
|
||||
Ok(metrics) => {
|
||||
let _ = sender.send(AppEvent::MetricsUpdated {
|
||||
host,
|
||||
smart: None,
|
||||
services: Some(metrics),
|
||||
backup: None,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(%error, "failed to parse service metrics");
|
||||
let _ = sender.send(AppEvent::MetricsFailed {
|
||||
host,
|
||||
error: format!("service metrics parse error: {error:#}"),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
},
|
||||
AgentType::Backup => match serde_json::from_value::<BackupMetrics>(payload.clone()) {
|
||||
Ok(metrics) => {
|
||||
let _ = sender.send(AppEvent::MetricsUpdated {
|
||||
host,
|
||||
smart: None,
|
||||
services: None,
|
||||
backup: Some(metrics),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(%error, "failed to parse backup metrics");
|
||||
let _ = sender.send(AppEvent::MetricsFailed {
|
||||
host,
|
||||
error: format!("backup metrics parse error: {error:#}"),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum AgentType {
|
||||
Smart,
|
||||
Service,
|
||||
Backup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MetricsEnvelope {
|
||||
hostname: String,
|
||||
#[serde(rename = "agent_type")]
|
||||
agent_type: AgentType,
|
||||
timestamp: u64,
|
||||
metrics: Value,
|
||||
}
|
||||
|
||||
fn ensure_default_config(cli: &Cli) -> Result<()> {
|
||||
if let Some(path) = cli.config.as_ref() {
|
||||
ensure_config_at(path, false)?;
|
||||
} else {
|
||||
let default_path = Path::new("config/dashboard.toml");
|
||||
if !default_path.exists() {
|
||||
generate_config_templates(Path::new("config"), false)?;
|
||||
println!("Created default configuration in ./config");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_config_at(path: &Path, force: bool) -> Result<()> {
|
||||
if path.exists() && !force {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create directory {}", parent.display()))?;
|
||||
}
|
||||
|
||||
write_template(path.to_path_buf(), DASHBOARD_TEMPLATE, force, "dashboard")?;
|
||||
|
||||
let hosts_path = parent.join("hosts.toml");
|
||||
if !hosts_path.exists() || force {
|
||||
write_template(hosts_path, HOSTS_TEMPLATE, force, "hosts")?;
|
||||
}
|
||||
println!(
|
||||
"Created configuration templates in {} (dashboard: {})",
|
||||
parent.display(),
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
return Err(anyhow!("invalid configuration path {}", path.display()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_config_templates(target_dir: &Path, force: bool) -> Result<()> {
|
||||
if !target_dir.exists() {
|
||||
fs::create_dir_all(target_dir)
|
||||
.with_context(|| format!("failed to create directory {}", target_dir.display()))?;
|
||||
}
|
||||
|
||||
write_template(
|
||||
target_dir.join("dashboard.toml"),
|
||||
DASHBOARD_TEMPLATE,
|
||||
force,
|
||||
"dashboard",
|
||||
)?;
|
||||
write_template(
|
||||
target_dir.join("hosts.toml"),
|
||||
HOSTS_TEMPLATE,
|
||||
force,
|
||||
"hosts",
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"Configuration templates written to {}",
|
||||
target_dir.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_template(path: PathBuf, contents: &str, force: bool, name: &str) -> Result<()> {
|
||||
if path.exists() && !force {
|
||||
return Err(anyhow!(
|
||||
"{} template already exists at {} (use --force to overwrite)",
|
||||
name,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
fs::write(&path, contents)
|
||||
.with_context(|| format!("failed to write {} template to {}", name, path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const DASHBOARD_TEMPLATE: &str = r#"# CM Dashboard configuration
|
||||
|
||||
[hosts]
|
||||
# default_host = "srv01"
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "srv01"
|
||||
base_url = "http://srv01.local"
|
||||
enabled = true
|
||||
# metadata = { rack = "R1" }
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "labbox"
|
||||
base_url = "http://labbox.local"
|
||||
enabled = true
|
||||
|
||||
[dashboard]
|
||||
tick_rate_ms = 250
|
||||
history_duration_minutes = 60
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "nvme"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "services"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "backup"
|
||||
enabled = true
|
||||
|
||||
[[dashboard.widgets]]
|
||||
id = "alerts"
|
||||
enabled = true
|
||||
|
||||
[filesystem]
|
||||
# cache_dir = "/var/lib/cm-dashboard/cache"
|
||||
# history_dir = "/var/lib/cm-dashboard/history"
|
||||
"#;
|
||||
|
||||
const HOSTS_TEMPLATE: &str = r#"# Optional separate hosts configuration
|
||||
|
||||
[hosts]
|
||||
# default_host = "srv01"
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "srv01"
|
||||
base_url = "http://srv01.local"
|
||||
enabled = true
|
||||
|
||||
[[hosts.hosts]]
|
||||
name = "labbox"
|
||||
base_url = "http://labbox.local"
|
||||
enabled = true
|
||||
"#;
|
||||
51
src/ui/alerts.rs
Normal file
51
src/ui/alerts.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
|
||||
pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
||||
let block = Block::default()
|
||||
.title("Alerts")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::LightRed));
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
if hosts.is_empty() {
|
||||
lines.push(Line::from("No hosts configured"));
|
||||
} else {
|
||||
for host in hosts {
|
||||
if let Some(error) = &host.last_error {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(&host.name, Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(": "),
|
||||
Span::styled(error, Style::default().fg(Color::Red)),
|
||||
]));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(smart) = host.smart.as_ref() {
|
||||
if let Some(issue) = smart.issues.first() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(&host.name, Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(": "),
|
||||
Span::styled(issue, Style::default().fg(Color::Yellow)),
|
||||
]));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(&host.name, Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(": OK"),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true }).block(block);
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
62
src/ui/backup.rs
Normal file
62
src/ui/backup.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
let block = Block::default()
|
||||
.title("Backups")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::LightGreen));
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.backup.as_ref() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Host: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(data.name.clone()),
|
||||
]));
|
||||
lines.push(Line::from(format!("Status: {:?}", metrics.overall_status)));
|
||||
|
||||
if let Some(last_success) = metrics.backup.last_success.as_ref() {
|
||||
lines.push(Line::from(format!(
|
||||
"Last success: {}",
|
||||
last_success.format("%Y-%m-%d %H:%M:%S")
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(last_failure) = metrics.backup.last_failure.as_ref() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Last failure: ", Style::default().fg(Color::Red)),
|
||||
Span::raw(last_failure.format("%Y-%m-%d %H:%M:%S").to_string()),
|
||||
]));
|
||||
}
|
||||
|
||||
lines.push(Line::from(format!(
|
||||
"Snapshots: {} • Size: {:.1} GiB",
|
||||
metrics.backup.snapshot_count, metrics.backup.size_gb
|
||||
)));
|
||||
|
||||
lines.push(Line::from(format!(
|
||||
"Pending jobs: {} (enabled: {})",
|
||||
metrics.service.pending_jobs, metrics.service.enabled
|
||||
)));
|
||||
} else {
|
||||
lines.push(Line::from(format!(
|
||||
"Host {} awaiting backup metrics",
|
||||
data.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => lines.push(Line::from("No hosts configured")),
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true }).block(block);
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
190
src/ui/dashboard.rs
Normal file
190
src/ui/dashboard.rs
Normal file
@ -0,0 +1,190 @@
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
use super::{alerts, backup, memory, nvme, services};
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App) {
|
||||
let host_summaries = app.host_display_data();
|
||||
let primary_host = app.active_host_display();
|
||||
|
||||
let root_block = Block::default().title(Span::styled(
|
||||
"CM Dashboard",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
|
||||
let size = frame.size();
|
||||
frame.render_widget(root_block, size);
|
||||
|
||||
let outer = inner_rect(size);
|
||||
|
||||
let vertical_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(30),
|
||||
])
|
||||
.split(outer);
|
||||
|
||||
let top = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(vertical_chunks[0]);
|
||||
|
||||
let middle = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
let bottom = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(vertical_chunks[2]);
|
||||
|
||||
nvme::render(frame, primary_host.as_ref(), top[0]);
|
||||
services::render(frame, primary_host.as_ref(), top[1]);
|
||||
memory::render(frame, primary_host.as_ref(), middle[0]);
|
||||
backup::render(frame, primary_host.as_ref(), middle[1]);
|
||||
alerts::render(frame, &host_summaries, bottom[0]);
|
||||
render_status(frame, app, bottom[1]);
|
||||
|
||||
if app.help_visible() {
|
||||
render_help(frame, size);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_status(frame: &mut Frame, app: &App, area: Rect) {
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Paragraph, Wrap};
|
||||
|
||||
let mut lines = Vec::new();
|
||||
lines.push(Line::from(app.status_text().to_string()));
|
||||
|
||||
if app.zmq_connected() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
"Data source: ",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled("ZMQ", Style::default().fg(Color::Green)),
|
||||
]));
|
||||
} else {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
"Data source: ",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled("ZMQ (disconnected)", Style::default().fg(Color::Red)),
|
||||
]));
|
||||
}
|
||||
|
||||
if let Some((index, host)) = app.active_host_info() {
|
||||
lines.push(Line::from(format!(
|
||||
"Active host: {} ({}/{})",
|
||||
host.name,
|
||||
index + 1,
|
||||
app.hosts().len()
|
||||
)));
|
||||
} else {
|
||||
lines.push(Line::from("Active host: —"));
|
||||
}
|
||||
|
||||
if let Some(path) = app.active_config_path() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Config: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(path.display().to_string()),
|
||||
]));
|
||||
}
|
||||
|
||||
let retention = app.history().retention();
|
||||
lines.push(Line::from(format!(
|
||||
"History retention ≈ {}s",
|
||||
retention.as_secs()
|
||||
)));
|
||||
|
||||
if let Some(config) = app.config() {
|
||||
if let Some(default_host) = &config.hosts.default_host {
|
||||
lines.push(Line::from(format!("Default host: {}", default_host)));
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true }).block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Status",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(ratatui::widgets::Borders::ALL),
|
||||
);
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn inner_rect(area: Rect) -> Rect {
|
||||
Rect {
|
||||
x: area.x + 1,
|
||||
y: area.y + 1,
|
||||
width: area.width.saturating_sub(2),
|
||||
height: area.height.saturating_sub(2),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_help(frame: &mut Frame, area: Rect) {
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
|
||||
|
||||
let help_area = centered_rect(60, 40, area);
|
||||
let lines = vec![
|
||||
Line::from("Keyboard Shortcuts"),
|
||||
Line::from("←/→ or h/l: Switch active host"),
|
||||
Line::from("r: Manual refresh status"),
|
||||
Line::from("?: Toggle this help"),
|
||||
Line::from("q / Esc: Quit dashboard"),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.title(Span::styled(
|
||||
"Help",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::Black));
|
||||
|
||||
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true }).block(block);
|
||||
|
||||
frame.render_widget(Clear, help_area);
|
||||
frame.render_widget(paragraph, help_area);
|
||||
}
|
||||
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
|
||||
let vertical = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let horizontal = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(vertical[1]);
|
||||
|
||||
horizontal[1]
|
||||
}
|
||||
56
src/ui/memory.rs
Normal file
56
src/ui/memory.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
let block = Block::default()
|
||||
.title("Memory Optimization")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::LightMagenta));
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.services.as_ref() {
|
||||
let summary = &metrics.summary;
|
||||
let usage_ratio = if summary.memory_quota_mb > 0.0 {
|
||||
(summary.memory_used_mb / summary.memory_quota_mb) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Host: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(data.name.clone()),
|
||||
]));
|
||||
|
||||
lines.push(Line::from(format!(
|
||||
"Memory used: {:.1} / {:.1} MiB ({:.1}%)",
|
||||
summary.memory_used_mb, summary.memory_quota_mb, usage_ratio
|
||||
)));
|
||||
|
||||
if let Some(last_success) = data.last_success.as_ref() {
|
||||
lines.push(Line::from(format!(
|
||||
"Last update: {}",
|
||||
last_success.format("%H:%M:%S")
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
lines.push(Line::from(format!(
|
||||
"Host {} awaiting service metrics",
|
||||
data.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => lines.push(Line::from("No hosts configured")),
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true }).block(block);
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
8
src/ui/mod.rs
Normal file
8
src/ui/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod alerts;
|
||||
pub mod backup;
|
||||
pub mod dashboard;
|
||||
pub mod memory;
|
||||
pub mod nvme;
|
||||
pub mod services;
|
||||
|
||||
pub use dashboard::render;
|
||||
58
src/ui/nvme.rs
Normal file
58
src/ui/nvme.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
let block = Block::default()
|
||||
.title("NVMe Health")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::LightCyan));
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.smart.as_ref() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Host: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(data.name.clone()),
|
||||
]));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Status: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(metrics.status.clone()),
|
||||
]));
|
||||
lines.push(Line::from(format!(
|
||||
"Drives healthy/warn/crit: {}/{}/{}",
|
||||
metrics.summary.healthy, metrics.summary.warning, metrics.summary.critical
|
||||
)));
|
||||
lines.push(Line::from(format!(
|
||||
"Capacity used: {:.1} / {:.1} GiB",
|
||||
metrics.summary.capacity_used_gb, metrics.summary.capacity_total_gb
|
||||
)));
|
||||
|
||||
if let Some(issue) = metrics.issues.first() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Issue: ", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(issue.clone()),
|
||||
]));
|
||||
}
|
||||
} else {
|
||||
lines.push(Line::from(format!(
|
||||
"Host {} has no SMART data yet",
|
||||
data.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
lines.push(Line::from("No hosts configured"));
|
||||
}
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true }).block(block);
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
54
src/ui/services.rs
Normal file
54
src/ui/services.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
let block = Block::default()
|
||||
.title("Services")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.services.as_ref() {
|
||||
let summary = &metrics.summary;
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Host: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(data.name.clone()),
|
||||
]));
|
||||
lines.push(Line::from(format!(
|
||||
"Services healthy/degraded/failed: {}/{}/{}",
|
||||
summary.healthy, summary.degraded, summary.failed
|
||||
)));
|
||||
lines.push(Line::from(format!(
|
||||
"CPU top service: {:.1}%",
|
||||
metrics
|
||||
.services
|
||||
.iter()
|
||||
.map(|svc| svc.cpu_percent)
|
||||
.fold(0.0_f32, f32::max)
|
||||
)));
|
||||
lines.push(Line::from(format!(
|
||||
"Total memory: {:.1} / {:.1} MiB",
|
||||
summary.memory_used_mb, summary.memory_quota_mb
|
||||
)));
|
||||
} else {
|
||||
lines.push(Line::from(format!(
|
||||
"Host {} has no service metrics yet",
|
||||
data.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => lines.push(Line::from("No hosts configured")),
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: true }).block(block);
|
||||
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user