diff --git a/cmd/status/metrics.go b/cmd/status/metrics.go index 2b24bba..86f958b 100644 --- a/cmd/status/metrics.go +++ b/cmd/status/metrics.go @@ -22,18 +22,19 @@ type MetricsSnapshot struct { HealthScore int // 0-100 system health score HealthScoreMsg string // Brief explanation - CPU CPUStatus - GPU []GPUStatus - Memory MemoryStatus - Disks []DiskStatus - DiskIO DiskIOStatus - Network []NetworkStatus - Proxy ProxyStatus - Batteries []BatteryStatus - Thermal ThermalStatus - Sensors []SensorReading - Bluetooth []BluetoothDevice - TopProcesses []ProcessInfo + CPU CPUStatus + GPU []GPUStatus + Memory MemoryStatus + Disks []DiskStatus + DiskIO DiskIOStatus + Network []NetworkStatus + NetworkHistory NetworkHistory + Proxy ProxyStatus + Batteries []BatteryStatus + Thermal ThermalStatus + Sensors []SensorReading + Bluetooth []BluetoothDevice + TopProcesses []ProcessInfo } type HardwareInfo struct { @@ -104,6 +105,12 @@ type NetworkStatus struct { TxRateMBs float64 IP string } +type NetworkHistory struct { + RxHistory []float64 + TxHistory []float64 +} + +const NetworkHistorySize = 20 // number of checks to keep type ProxyStatus struct { Enabled bool @@ -156,6 +163,7 @@ type Collector struct { // Fast metrics (1s). prevNet map[string]net.IOCountersStat lastNetAt time.Time + netHistory NetworkHistory lastGPUAt time.Time cachedGPU []GPUStatus prevDiskIO disk.IOCountersStat @@ -263,6 +271,7 @@ func (c *Collector) Collect() (MetricsSnapshot, error) { Disks: diskStats, DiskIO: diskIO, Network: netStats, + NetworkHistory: c.netHistory, Proxy: proxyStats, Batteries: batteryStats, Thermal: thermalStats, diff --git a/cmd/status/metrics_network.go b/cmd/status/metrics_network.go index 00c94e6..f38f69d 100644 --- a/cmd/status/metrics_network.go +++ b/cmd/status/metrics_network.go @@ -70,6 +70,20 @@ func (c *Collector) collectNetwork(now time.Time) ([]NetworkStatus, error) { result = result[:3] } + var totalRx, totalTx float64 + for _, r := range result { + totalRx += r.RxRateMBs + totalTx += r.TxRateMBs + } + c.netHistory.RxHistory = append(c.netHistory.RxHistory, totalRx) + c.netHistory.TxHistory = append(c.netHistory.TxHistory, totalTx) + if len(c.netHistory.RxHistory) > NetworkHistorySize { + c.netHistory.RxHistory = c.netHistory.RxHistory[len(c.netHistory.RxHistory)-NetworkHistorySize:] + } + if len(c.netHistory.TxHistory) > NetworkHistorySize { + c.netHistory.TxHistory = c.netHistory.TxHistory[len(c.netHistory.TxHistory)-NetworkHistorySize:] + } + return result, nil } diff --git a/cmd/status/view.go b/cmd/status/view.go index ce2a174..47991da 100644 --- a/cmd/status/view.go +++ b/cmd/status/view.go @@ -208,7 +208,7 @@ func buildCards(m MetricsSnapshot, _ int) []cardData { renderDiskCard(m.Disks, m.DiskIO), renderBatteryCard(m.Batteries, m.Thermal), renderProcessCard(m.TopProcesses), - renderNetworkCard(m.Network, m.Proxy), + renderNetworkCard(m.Network, m.NetworkHistory, m.Proxy), } if hasSensorData(m.Sensors) { cards = append(cards, renderSensorsCard(m.Sensors)) @@ -425,7 +425,7 @@ func miniBar(percent float64) string { return colorizePercent(percent, strings.Repeat("▮", filled)+strings.Repeat("▯", 5-filled)) } -func renderNetworkCard(netStats []NetworkStatus, proxy ProxyStatus) cardData { +func renderNetworkCard(netStats []NetworkStatus, history NetworkHistory, proxy ProxyStatus) cardData { var lines []string var totalRx, totalTx float64 var primaryIP string @@ -441,10 +441,11 @@ func renderNetworkCard(netStats []NetworkStatus, proxy ProxyStatus) cardData { if len(netStats) == 0 { lines = []string{subtleStyle.Render("Collecting...")} } else { - rxBar := netBar(totalRx) - txBar := netBar(totalTx) - lines = append(lines, fmt.Sprintf("Down %s %s", rxBar, formatRate(totalRx))) - lines = append(lines, fmt.Sprintf("Up %s %s", txBar, formatRate(totalTx))) + // sparkline graphs + rxSparkline := sparkline(history.RxHistory, totalRx) + txSparkline := sparkline(history.TxHistory, totalTx) + lines = append(lines, fmt.Sprintf("Down %s %s", rxSparkline, formatRate(totalRx))) + lines = append(lines, fmt.Sprintf("Up %s %s", txSparkline, formatRate(totalTx))) // Show proxy and IP on one line. var infoParts []string if proxy.Enabled { @@ -475,6 +476,57 @@ func netBar(rate float64) string { return okStyle.Render(bar) } +// 8 levels: ▁▂▃▄▅▆▇█ +func sparkline(history []float64, current float64) string { + const width = 16 + blocks := []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} + + data := make([]float64, 0, width) + if len(history) > 0 { + // Take the most recent points. + start := 0 + if len(history) > width { + start = len(history) - width + } + data = append(data, history[start:]...) + } + // padding with zeros at the start + for len(data) < width { + data = append([]float64{0}, data...) + } + if len(data) > width { + data = data[len(data)-width:] + } + + maxVal := 0.1 + for _, v := range data { + if v > maxVal { + maxVal = v + } + } + + var builder strings.Builder + for _, v := range data { + level := int((v / maxVal) * float64(len(blocks)-1)) + if level < 0 { + level = 0 + } + if level >= len(blocks) { + level = len(blocks) - 1 + } + builder.WriteRune(blocks[level]) + } + + result := builder.String() + if current > 8 { + return dangerStyle.Render(result) + } + if current > 3 { + return warnStyle.Render(result) + } + return okStyle.Render(result) +} + func renderBatteryCard(batts []BatteryStatus, thermal ThermalStatus) cardData { var lines []string if len(batts) == 0 {