From 5e79e497d3682741ce192d245fd193322c03b85a Mon Sep 17 00:00:00 2001 From: Mahyar Mirrashed <59240843+mahyarmirrashed@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:37:27 -0500 Subject: [PATCH] cmd/tailscale/cli: show last seen time on status command (#16588) Add a last seen time on the cli's status command, similar to the web portal. Before: ``` 100.xxx.xxx.xxx tailscale-operator tagged-devices linux offline ``` After: ``` 100.xxx.xxx.xxx tailscale-operator tagged-devices linux offline, last seen 20d ago ``` Fixes #16584 Signed-off-by: Mahyar Mirrashed --- cmd/tailscale/cli/cli.go | 17 +++++++++++++++++ cmd/tailscale/cli/exitnode.go | 6 ++++-- cmd/tailscale/cli/status.go | 6 +++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index dfc8f3249..5206fdd58 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -18,6 +18,7 @@ import ( "strings" "sync" "text/tabwriter" + "time" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" @@ -538,3 +539,19 @@ func jsonDocsWalk(cmd *ffcli.Command) *commandDoc { } return res } + +func lastSeenFmt(t time.Time) string { + if t.IsZero() { + return "" + } + d := max(time.Since(t), time.Minute) // at least 1 minute + + switch { + case d < time.Hour: + return fmt.Sprintf(", last seen %dm ago", int(d.Minutes())) + case d < 24*time.Hour: + return fmt.Sprintf(", last seen %dh ago", int(d.Hours())) + default: + return fmt.Sprintf(", last seen %dd ago", int(d.Hours()/24)) + } +} diff --git a/cmd/tailscale/cli/exitnode.go b/cmd/tailscale/cli/exitnode.go index b153f096d..b47b9f0bd 100644 --- a/cmd/tailscale/cli/exitnode.go +++ b/cmd/tailscale/cli/exitnode.go @@ -173,11 +173,13 @@ func hasAnyExitNodeSuggestions(peers []*ipnstate.PeerStatus) bool { // a peer. If there is no notable state, a - is returned. func peerStatus(peer *ipnstate.PeerStatus) string { if !peer.Active { + lastseen := lastSeenFmt(peer.LastSeen) + if peer.ExitNode { - return "selected but offline" + return "selected but offline" + lastseen } if !peer.Online { - return "offline" + return "offline" + lastseen } } diff --git a/cmd/tailscale/cli/status.go b/cmd/tailscale/cli/status.go index 97f6708db..94e0977fe 100644 --- a/cmd/tailscale/cli/status.go +++ b/cmd/tailscale/cli/status.go @@ -164,7 +164,7 @@ func runStatus(ctx context.Context, args []string) error { anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0 var offline string if !ps.Online { - offline = "; offline" + offline = "; offline" + lastSeenFmt(ps.LastSeen) } if !ps.Active { if ps.ExitNode { @@ -174,7 +174,7 @@ func runStatus(ctx context.Context, args []string) error { } else if anyTraffic { f("idle" + offline) } else if !ps.Online { - f("offline") + f("offline" + lastSeenFmt(ps.LastSeen)) } else { f("-") } @@ -193,7 +193,7 @@ func runStatus(ctx context.Context, args []string) error { f("peer-relay %s", ps.PeerRelay) } if !ps.Online { - f("; offline") + f(offline) } } if anyTraffic {