From 747de27a00f7f2c7e55fbb753beb2046a6f988c9 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Fri, 28 Aug 2026 14:02:48 +0200 Subject: [PATCH] Show device state in device list Show each device's run state and used and total storage in table and JSON output. --- internal/api/devices.go | 11 +++++--- internal/device/device.go | 50 +++++++++++++++++++++++++++++++--- internal/device/device_test.go | 14 +++++----- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/internal/api/devices.go b/internal/api/devices.go index a6f849b..8fffec8 100644 --- a/internal/api/devices.go +++ b/internal/api/devices.go @@ -6,10 +6,13 @@ import ( ) type DeviceEntry struct { - Imei string `json:"imei"` - Name *string `json:"name"` - FleetId int64 `json:"fleet_id"` - LastSeenAt *string `json:"last_seen_at"` + Imei string `json:"imei"` + Name *string `json:"name"` + FleetId int64 `json:"fleet_id"` + LastSeenAt *string `json:"last_seen_at"` + RunState *string `json:"run_state"` + StorageUsed *uint64 `json:"storage_used"` + StorageTotal *uint64 `json:"storage_total"` } func FetchDevices(invocation Invocation) ([]DeviceEntry, error) { diff --git a/internal/device/device.go b/internal/device/device.go index bdd9eb7..c6939d5 100644 --- a/internal/device/device.go +++ b/internal/device/device.go @@ -161,9 +161,13 @@ func List(invocation api.Invocation, arguments []string) error { imeiWidth := len("IMEI") nameWidth := len("NAME") fleetWidth := len("FLEET") + runStateWidth := len("RUN STATE") + storageWidth := len("STORAGE") imeiValues := make([]string, len(filtered)) nameValues := make([]string, len(filtered)) fleetValues := make([]string, len(filtered)) + runStateValues := make([]string, len(filtered)) + storageValues := make([]string, len(filtered)) lastSeenValues := make([]string, len(filtered)) for index, device := range filtered { @@ -202,27 +206,65 @@ func List(invocation api.Invocation, arguments []string) error { fleetName = "-" } + runState := "-" + + if device.RunState != nil { + runState = *device.RunState + } + + storage := "-" + + if device.StorageUsed != nil && device.StorageTotal != nil { + storage = fmt.Sprintf("%s / %s", formatByteCount(*device.StorageUsed), formatByteCount(*device.StorageTotal)) + } + imeiValues[index] = api.Printable(device.Imei) nameValues[index] = api.Printable(name) fleetValues[index] = api.Printable(fleetName) + runStateValues[index] = runState + storageValues[index] = storage lastSeenValues[index] = lastSeen imeiWidth = max(imeiWidth, len(imeiValues[index])) nameWidth = max(nameWidth, len(nameValues[index])) fleetWidth = max(fleetWidth, len(fleetValues[index])) + runStateWidth = max(runStateWidth, len(runStateValues[index])) + storageWidth = max(storageWidth, len(storageValues[index])) } - fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %s\n", - imeiWidth, "IMEI", nameWidth, "NAME", fleetWidth, "FLEET", "LAST SEEN") + fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %-*s %-*s %s\n", + imeiWidth, "IMEI", nameWidth, "NAME", fleetWidth, "FLEET", runStateWidth, "RUN STATE", + storageWidth, "STORAGE", "LAST SEEN") for index := range filtered { - fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %s\n", + fmt.Fprintf(invocation.Out, "%-*s %-*s %-*s %-*s %-*s %s\n", imeiWidth, imeiValues[index], nameWidth, nameValues[index], fleetWidth, fleetValues[index], - lastSeenValues[index]) + runStateWidth, runStateValues[index], storageWidth, storageValues[index], lastSeenValues[index]) } return nil } +func formatByteCount(byteCount uint64) string { + units := [...]string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} + value := float64(byteCount) + unit := 0 + + for value >= 1024 && unit < len(units)-1 { + value /= 1024 + unit++ + } + + if unit == 0 { + return fmt.Sprintf("%d B", byteCount) + } + + if value >= 10 { + return fmt.Sprintf("%.0f %s", value, units[unit]) + } + + return fmt.Sprintf("%.1f %s", value, units[unit]) +} + func Rename(invocation api.Invocation, arguments []string) error { if len(arguments) != 2 { return errors.New("device rename takes an IMEI and a new name, quoted if it has spaces") diff --git a/internal/device/device_test.go b/internal/device/device_test.go index def2f19..6f5c6bf 100644 --- a/internal/device/device_test.go +++ b/internal/device/device_test.go @@ -14,9 +14,9 @@ import ( func TestDeviceList(t *testing.T) { now := time.Now() - devices := fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q},`+ - `{"imei":"222222222222222","name":null,"fleet_id":4,"last_seen_at":%q},`+ - `{"imei":"333333333333333","name":"shed","fleet_id":3,"last_seen_at":null}]`, + devices := fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q,"run_state":"running","storage_used":128,"storage_total":1024},`+ + `{"imei":"222222222222222","name":null,"fleet_id":4,"last_seen_at":%q,"run_state":"crashed","storage_used":1536,"storage_total":1048576},`+ + `{"imei":"333333333333333","name":"shed","fleet_id":3,"last_seen_at":null,"run_state":null,"storage_used":null,"storage_total":null}]`, now.Add(-time.Minute).Format(time.RFC3339), now.Add(-3*time.Hour).Format(time.RFC3339)) fleets := `[{"id":3,"name":"pilot","owner":true},{"id":4,"name":"workshop","owner":true},{"id":5,"name":"empty","owner":true}]` @@ -31,17 +31,17 @@ func TestDeviceList(t *testing.T) { fleets string refusal string }{ - {name: "table", wantShown: []string{"IMEI NAME FLEET LAST SEEN", "roof", "pilot", "just now", "-", "workshop", "3 h ago", "never"}}, + {name: "table", wantShown: []string{"IMEI NAME FLEET", "RUN STATE", "STORAGE", "LAST SEEN", "roof", "pilot", "running", "128 B / 1.0 KiB", "workshop", "crashed", "1.5 KiB / 1.0 MiB", "3 h ago", "never"}}, {name: "filtered", arguments: []string{"3"}, wantShown: []string{"111111111111111", "333333333333333"}, wantHidden: []string{"222222222222222", "workshop"}}, - {name: "json flag anywhere", arguments: []string{"3", "--json"}, wantShown: []string{`"imei":"111111111111111"`, `"fleet_id":3`, `"last_seen_at":`}, wantHidden: []string{"LAST SEEN", "222222222222222", `"reported_state"`, `"run_state"`, `"storage_used"`}}, + {name: "json flag anywhere", arguments: []string{"3", "--json"}, wantShown: []string{`"imei":"111111111111111"`, `"fleet_id":3`, `"last_seen_at":`, `"run_state":"running"`, `"storage_used":128`, `"storage_total":1024`}, wantHidden: []string{"LAST SEEN", "222222222222222", `"reported_state"`}}, {name: "empty fleet", arguments: []string{"5"}, wantExact: "No devices in that fleet.\n"}, {name: "no devices", devices: `[]`, fleets: `[]`, wantExact: "No devices yet.\n"}, {name: "server refusal", refusal: "devices unavailable", wantError: "devices unavailable"}, {name: "unknown fleet", arguments: []string{"9"}, wantError: "no such fleet"}, {name: "two ids", arguments: []string{"3", "4"}, wantError: "takes at most one fleet id"}, {name: "wordy id", arguments: []string{"pilot"}, wantError: "shown by fleet list"}, - {name: "an unreadable last seen time leaves the rest of the table", devices: `[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":"yesterday"}]`, wantShown: []string{"111111111111111 roof pilot unknown"}}, - {name: "a fleet the list does not name", devices: `[{"imei":"888888888888888","name":"orphan","fleet_id":99}]`, wantShown: []string{"888888888888888 orphan - never"}}, + {name: "an unreadable last seen time leaves the rest of the table", devices: `[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":"yesterday","run_state":"stopped","storage_used":0,"storage_total":1024}]`, wantShown: []string{"111111111111111 roof pilot stopped", "0 B / 1.0 KiB", "unknown"}}, + {name: "a fleet the list does not name", devices: `[{"imei":"888888888888888","name":"orphan","fleet_id":99}]`, wantShown: []string{"888888888888888 orphan -", "never"}}, {name: "a name with control characters is escaped", devices: `[{"imei":"111111111111111","name":"\u001b[2K\rhidden","fleet_id":3}]`, wantShown: []string{`\x1b[2K\rhidden`}, wantHidden: []string{"\x1b"}}, {name: "minutes ago", devices: fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q}]`, now.Add(-12*time.Minute).Format(time.RFC3339)), wantShown: []string{"12 min ago"}}, {name: "days ago", devices: fmt.Sprintf(`[{"imei":"111111111111111","name":"roof","fleet_id":3,"last_seen_at":%q}]`, now.Add(-49*time.Hour).Format(time.RFC3339)), wantShown: []string{"2 d ago"}},