Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## [0.1.5] - 2026-07-08

### Added

- Enriched JSON snapshot output -- `--once --json` now includes `status`, `timestamp` (RFC3339), `download_human`, and `upload_human` fields for self-contained script consumption.
- Refresh interval permanently displayed in footer when non-default (e.g. "every 5s").
- Extended sampling range up to 5 minutes (30s, 60s, 300s) for long-term overnight monitoring.
- Bits indicator -- footer shows `[bits]` label when bits mode is active for clear differentiation from bytes mode.

## [0.1.4] - 2026-07-07

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.4
0.1.5
18 changes: 12 additions & 6 deletions cmd/flow/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,19 @@ func runOnce(col *collector.Collector, smp *sampler.Sampler, refresh time.Durati
cancel()

if asJSON {
down := ui.FormatBpsExt(s.DownBps, ui.UnitAuto, bits)
up := ui.FormatBpsExt(s.UpBps, ui.UnitAuto, bits)
out := map[string]interface{}{
"download_bps": s.DownBps,
"upload_bps": s.UpBps,
"peak_down_bps": s.DownBps, // peak = current in one-shot mode
"peak_up_bps": s.UpBps,
"interface": s.Interface,
"unit_display": autoUnitExt(s.DownBps, bits),
"status": "ok",
"timestamp": time.Now().Format(time.RFC3339),
"interface": s.Interface,
"download_bps": s.DownBps,
"upload_bps": s.UpBps,
"download_human": down,
"upload_human": up,
"peak_down_bps": s.DownBps,
"peak_up_bps": s.UpBps,
"unit_display": autoUnitExt(s.DownBps, bits),
Comment on lines +186 to +192

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

When bits mode is active, unit_display returns a bits-based unit (e.g., Gb/s), and download_human displays the speed in bits/sec. However, download_bps, upload_bps, peak_down_bps, and peak_up_bps still contain the raw bytes/sec values (s.DownBps and s.UpBps). This creates an inconsistency in the JSON output where the raw numeric values do not match the unit specified in unit_display. Consider scaling these numeric values by 8 when bits is true, or renaming/adding separate fields to clarify the unit.

}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
Expand Down
6 changes: 6 additions & 0 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@ func (m *Model) adjustRefreshInterval(faster bool) {
500 * time.Millisecond,
1 * time.Second,
2 * time.Second,
3 * time.Second,
5 * time.Second,
10 * time.Second,
30 * time.Second,
60 * time.Second,
300 * time.Second,
}

idx := -1
Expand Down
13 changes: 13 additions & 0 deletions internal/ui/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ func dashboardContentLines(m Model, mode ViewMode) []string {
if m.paused {
ifaceStr += theme.Muted().Render(" paused")
}
if m.bitsMode {
ifaceStr += theme.Muted().Render(" [bits]")
}
if m.refreshInterval != 100*time.Millisecond {
ifaceStr += theme.Muted().Render(" " + formatInterval(m.refreshInterval))
Comment on lines +452 to +453

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Avoid hard-coding the 100ms default refresh interval in the view layer

Using 100*time.Millisecond here couples the UI to a specific default. If that default changes, this condition will drift out of sync. Please reference a shared constant (or a model field) instead so changes to the default automatically stay consistent with the display logic.

Suggested implementation:

		if m.refreshInterval != defaultRefreshInterval {
			ifaceStr += theme.Muted().Render("  " + formatInterval(m.refreshInterval))
		}
const defaultRefreshInterval = 100 * time.Millisecond

func formatInterval(d time.Duration) string {

If your project already defines a canonical default refresh interval (e.g. in a config or model package), you should:

  1. Remove the newly added defaultRefreshInterval constant from views.go.
  2. Import and reference the existing shared default (e.g. model.DefaultRefreshInterval) in the view condition instead of the local constant.

}
Comment on lines +452 to +454

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The default refresh interval of 100 * time.Millisecond is hardcoded here. If the default refresh interval is changed in the configuration or another part of the codebase in the future, this check will become out of sync. Consider defining a constant for the default refresh interval (e.g., in the config package or as a package-level constant) and referencing it here to improve maintainability.


renderKey := func(k, desc string) string {
return lipgloss.NewStyle().Foreground(lipgloss.Color(theme.GetAccentColor())).Bold(true).Render(k) + " " + theme.Muted().Render(desc)
Expand Down Expand Up @@ -701,6 +707,13 @@ func max(a, b int) int {
return b
}

func formatInterval(d time.Duration) string {
if d >= time.Second {
return fmt.Sprintf("every %ds", int(d.Seconds()))
}
Comment on lines +711 to +713

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using int(d.Seconds()) involves floating-point conversion, which is less idiomatic in Go and can theoretically lead to precision issues. It is more idiomatic and precise to use integer division: int(d / time.Second).

Suggested change
if d >= time.Second {
return fmt.Sprintf("every %ds", int(d.Seconds()))
}
if d >= time.Second {
return fmt.Sprintf("every %ds", int(d/time.Second))
}

return fmt.Sprintf("every %dms", d.Milliseconds())
}

func min(a, b int) int {
if a < b {
return a
Expand Down
Loading