From 8bcb83591828a0a7dc684f6849e5608edc7b4e68 Mon Sep 17 00:00:00 2001 From: Pei-Tang Huang Date: Sun, 14 Sep 2025 03:13:49 +0800 Subject: [PATCH 1/3] feat: implement Ping All feature with right-aligned status indicators Add comprehensive ping functionality: - Add 'G' shortcut to ping all servers simultaneously - Add right-aligned status indicators on server list - Show ping latency with adaptive formatting (<100ms as "##ms", >=100ms as "#.#s") - Responsive design adapts to window width changes - Color-coded status: green (up), red (down), orange (checking) - Update single server ping (g) to also show status --- README.md | 1 + internal/adapters/ui/handlers.go | 122 ++++++++++++++++++++++++++-- internal/adapters/ui/hint_bar.go | 2 +- internal/adapters/ui/server_list.go | 23 +++++- internal/adapters/ui/status_bar.go | 2 +- internal/adapters/ui/tui.go | 12 +++ internal/adapters/ui/utils.go | 98 +++++++++++++++++++++- internal/core/domain/server.go | 2 + 8 files changed, 247 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4b50d7f..498ebf1 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ make run | Enter | SSH into selected server | | c | Copy SSH command to clipboard | | g | Ping selected server | +| G | Ping all servers | | r | Refresh background data | | a | Add server | | e | Edit server | diff --git a/internal/adapters/ui/handlers.go b/internal/adapters/ui/handlers.go index 7b1becd..9e43b1d 100644 --- a/internal/adapters/ui/handlers.go +++ b/internal/adapters/ui/handlers.go @@ -66,6 +66,9 @@ func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey { case 'g': t.handlePingSelected() return nil + case 'G': + t.handlePingAll() + return nil case 'r': t.handleRefreshBackground() return nil @@ -234,22 +237,38 @@ func (t *tui) handleFormCancel() { t.returnToMain() } +const ( + statusUp = "up" + statusDown = "down" + statusChecking = "checking" +) + func (t *tui) handlePingSelected() { if server, ok := t.serverList.GetSelectedServer(); ok { alias := server.Alias + // Set checking status + server.PingStatus = statusChecking + t.pingStatuses[alias] = server + t.updateServerListWithPingStatus() + t.showStatusTemp(fmt.Sprintf("Pinging %s…", alias)) go func() { up, dur, err := t.serverService.Ping(server) t.app.QueueUpdateDraw(func() { - if err != nil { - t.showStatusTempColor(fmt.Sprintf("Ping %s: DOWN (%v)", alias, err), "#FF6B6B") - return - } - if up { - t.showStatusTempColor(fmt.Sprintf("Ping %s: UP (%s)", alias, dur), "#A0FFA0") - } else { - t.showStatusTempColor(fmt.Sprintf("Ping %s: DOWN", alias), "#FF6B6B") + // Update ping status + if ps, ok := t.pingStatuses[alias]; ok { + if err != nil || !up { + ps.PingStatus = statusDown + ps.PingLatency = 0 + t.showStatusTempColor(fmt.Sprintf("Ping %s: DOWN", alias), "#FF6B6B") + } else { + ps.PingStatus = statusUp + ps.PingLatency = dur + t.showStatusTempColor(fmt.Sprintf("Ping %s: UP (%s)", alias, dur), "#A0FFA0") + } + t.pingStatuses[alias] = ps + t.updateServerListWithPingStatus() } }) }() @@ -406,6 +425,93 @@ func (t *tui) returnToMain() { t.app.SetRoot(t.root, true) } +func (t *tui) updateServerListWithPingStatus() { + // Get current server list + query := "" + if t.searchVisible { + query = t.searchBar.InputField.GetText() + } + servers, _ := t.serverService.ListServers(query) + sortServersForUI(servers, t.sortMode) + + // Update ping status for each server + for i := range servers { + if ps, ok := t.pingStatuses[servers[i].Alias]; ok { + servers[i].PingStatus = ps.PingStatus + servers[i].PingLatency = ps.PingLatency + } + } + + t.serverList.UpdateServers(servers) +} + +func (t *tui) handlePingAll() { + query := "" + if t.searchVisible { + query = t.searchBar.InputField.GetText() + } + servers, err := t.serverService.ListServers(query) + if err != nil { + t.showStatusTempColor(fmt.Sprintf("Failed to get servers: %v", err), "#FF6B6B") + return + } + + if len(servers) == 0 { + t.showStatusTemp("No servers to ping") + return + } + + t.showStatusTemp(fmt.Sprintf("Pinging all %d servers…", len(servers))) + + // Clear existing statuses + t.pingStatuses = make(map[string]domain.Server) + + // Set all servers to checking status + for _, server := range servers { + s := server + s.PingStatus = statusChecking + t.pingStatuses[s.Alias] = s + } + t.updateServerListWithPingStatus() + + // Ping all servers concurrently + for _, server := range servers { + go func(srv domain.Server) { + up, dur, err := t.serverService.Ping(srv) + t.app.QueueUpdateDraw(func() { + if ps, ok := t.pingStatuses[srv.Alias]; ok { + if err != nil || !up { + ps.PingStatus = statusDown + ps.PingLatency = 0 + } else { + ps.PingStatus = statusUp + ps.PingLatency = dur + } + t.pingStatuses[srv.Alias] = ps + t.updateServerListWithPingStatus() + } + }) + }(server) + } + + // Show completion status after 3 seconds + go func() { + time.Sleep(3 * time.Second) + t.app.QueueUpdateDraw(func() { + upCount := 0 + downCount := 0 + for _, ps := range t.pingStatuses { + if ps.PingStatus == statusUp { + upCount++ + } else if ps.PingStatus == statusDown { + downCount++ + } + } + t.showStatusTempColor(fmt.Sprintf("Ping completed: %d UP, %d DOWN", upCount, downCount), "#A0FFA0") + }) + }() +} + // showStatusTemp displays a temporary message in the status bar (default green) and then restores the default text. func (t *tui) showStatusTemp(msg string) { if t.statusBar == nil { diff --git a/internal/adapters/ui/hint_bar.go b/internal/adapters/ui/hint_bar.go index de94973..9710976 100644 --- a/internal/adapters/ui/hint_bar.go +++ b/internal/adapters/ui/hint_bar.go @@ -22,6 +22,6 @@ import ( func NewHintBar() *tview.TextView { hint := tview.NewTextView().SetDynamicColors(true) hint.SetBackgroundColor(tcell.Color233) - hint.SetText("[#BBBBBB]Press [::b]/[-:-:b] to search… • ↑↓ Navigate • Enter SSH • c Copy SSH • g Ping • r Refresh • a Add • e Edit • t Tags • d Delete • p Pin/Unpin • s Sort[-]") + hint.SetText("[#BBBBBB]Press [::b]/[-:-:b] to search… • ↑↓ Navigate • Enter SSH • c Copy SSH • g/G Ping (All) • r Refresh • a Add • e Edit • t Tags • d Delete • p Pin/Unpin • s Sort[-]") return hint } diff --git a/internal/adapters/ui/server_list.go b/internal/adapters/ui/server_list.go index b175014..c8cf4d8 100644 --- a/internal/adapters/ui/server_list.go +++ b/internal/adapters/ui/server_list.go @@ -25,6 +25,7 @@ type ServerList struct { servers []domain.Server onSelection func(domain.Server) onSelectionChange func(domain.Server) + currentWidth int } func NewServerList() *ServerList { @@ -58,8 +59,12 @@ func (sl *ServerList) UpdateServers(servers []domain.Server) { sl.servers = servers sl.List.Clear() + // Get current width + _, _, width, _ := sl.List.GetInnerRect() //nolint:dogsled + sl.currentWidth = width + for i := range servers { - primary, secondary := formatServerLine(servers[i]) + primary, secondary := formatServerLine(servers[i], width) idx := i sl.List.AddItem(primary, secondary, 0, func() { if sl.onSelection != nil { @@ -76,6 +81,22 @@ func (sl *ServerList) UpdateServers(servers []domain.Server) { } } +// RefreshDisplay re-renders the list with current width +func (sl *ServerList) RefreshDisplay() { + _, _, width, _ := sl.List.GetInnerRect() //nolint:dogsled + if width != sl.currentWidth { + sl.currentWidth = width + // Save current selection + currentIdx := sl.List.GetCurrentItem() + // Re-render + sl.UpdateServers(sl.servers) + // Restore selection + if currentIdx >= 0 && currentIdx < sl.List.GetItemCount() { + sl.List.SetCurrentItem(currentIdx) + } + } +} + func (sl *ServerList) GetSelectedServer() (domain.Server, bool) { idx := sl.List.GetCurrentItem() if idx >= 0 && idx < len(sl.servers) { diff --git a/internal/adapters/ui/status_bar.go b/internal/adapters/ui/status_bar.go index d8ca0aa..2dc9cc8 100644 --- a/internal/adapters/ui/status_bar.go +++ b/internal/adapters/ui/status_bar.go @@ -20,7 +20,7 @@ import ( ) func DefaultStatusText() string { - return "[white]↑↓[-] Navigate • [white]Enter[-] SSH • [white]c[-] Copy SSH • [white]a[-] Add • [white]e[-] Edit • [white]g[-] Ping • [white]d[-] Delete • [white]p[-] Pin/Unpin • [white]/[-] Search • [white]q[-] Quit" + return "[white]↑↓[-] Navigate • [white]Enter[-] SSH • [white]c[-] Copy SSH • [white]a[-] Add • [white]e[-] Edit • [white]g/G[-] Ping (All) • [white]d[-] Delete • [white]p[-] Pin/Unpin • [white]/[-] Search • [white]q[-] Quit" } func NewStatusBar() *tview.TextView { diff --git a/internal/adapters/ui/tui.go b/internal/adapters/ui/tui.go index 6046071..f633d88 100644 --- a/internal/adapters/ui/tui.go +++ b/internal/adapters/ui/tui.go @@ -18,6 +18,7 @@ import ( "github.com/gdamore/tcell/v2" "go.uber.org/zap" + "github.com/Adembc/lazyssh/internal/core/domain" "github.com/Adembc/lazyssh/internal/core/ports" "github.com/rivo/tview" ) @@ -48,6 +49,8 @@ type tui struct { sortMode SortMode searchVisible bool + + pingStatuses map[string]domain.Server // stores ping status for each server } func NewTUI(logger *zap.SugaredLogger, ss ports.ServerService, version, commit string) App { @@ -57,6 +60,7 @@ func NewTUI(logger *zap.SugaredLogger, ss ports.ServerService, version, commit s serverService: ss, version: version, commit: commit, + pingStatuses: make(map[string]domain.Server), } } @@ -127,6 +131,14 @@ func (t *tui) buildLayout() *tui { func (t *tui) bindEvents() *tui { t.root.SetInputCapture(t.handleGlobalKeys) + + // Handle window resize + t.app.SetBeforeDrawFunc(func(screen tcell.Screen) bool { + // Refresh server list display on resize + t.serverList.RefreshDisplay() + return false + }) + return t } diff --git a/internal/adapters/ui/utils.go b/internal/adapters/ui/utils.go index 95a3996..e29acba 100644 --- a/internal/adapters/ui/utils.go +++ b/internal/adapters/ui/utils.go @@ -78,12 +78,102 @@ func pinnedIcon(pinnedAt time.Time) string { return "📌" // pinned } -func formatServerLine(s domain.Server) (primary, secondary string) { +func formatServerLine(s domain.Server, width int) (primary, secondary string) { icon := cellPad(pinnedIcon(s.PinnedAt), 2) - // Use a consistent color for alias; the icon reflects pinning - primary = fmt.Sprintf("%s [white::b]%-12s[-] [#AAAAAA]%-18s[-] [#888888]Last SSH: %s[-] %s", icon, s.Alias, s.Host, humanizeDuration(s.LastSeen), renderTagBadgesForList(s.Tags)) + + // Build main content + mainText := fmt.Sprintf("%s [white::b]%-12s[-] [#AAAAAA]%-18s[-] [#888888]Last SSH: %s[-] %s", + icon, s.Alias, s.Host, humanizeDuration(s.LastSeen), renderTagBadgesForList(s.Tags)) + + // Format ping status (4 chars max for value) + pingIndicator := "" + if s.PingStatus != "" { + switch s.PingStatus { + case "up": + if s.PingLatency > 0 { + ms := s.PingLatency.Milliseconds() + var statusText string + if ms < 100 { + statusText = fmt.Sprintf("%dms", ms) // e.g., "57ms" + } else { + // Format as #.#s for >= 100ms + seconds := float64(ms) / 1000.0 + statusText = fmt.Sprintf("%.1fs", seconds) // e.g., "0.3s", "1.5s" + } + // Ensure exactly 4 chars + statusText = fmt.Sprintf("%-4s", statusText) + pingIndicator = fmt.Sprintf("[#4AF626]● %s[-]", statusText) + } else { + pingIndicator = "[#4AF626]● UP [-]" + } + case "down": + pingIndicator = "[#FF6B6B]● DOWN[-]" + case "checking": + pingIndicator = "[#FFB86C]● ... [-]" + } + } + + // Calculate padding for right alignment + if pingIndicator != "" && width > 0 { + // Strip color codes to calculate real length + mainTextLen := len(stripSimpleColors(mainText)) + indicatorLen := 6 // "● XXXX" is always 6 display chars + + // Calculate padding needed + switch { + case width > 80: + // Wide screen: show full indicator + paddingLen := width - mainTextLen - indicatorLen // No margin, stick to right edge + if paddingLen < 1 { + paddingLen = 1 + } + padding := strings.Repeat(" ", paddingLen) + primary = fmt.Sprintf("%s%s%s", mainText, padding, pingIndicator) + case width > 60: + // Medium screen: show only dot + simplePingIndicator := "" + switch s.PingStatus { + case "up": + simplePingIndicator = "[#4AF626]●[-]" + case "down": + simplePingIndicator = "[#FF6B6B]●[-]" + case "checking": + simplePingIndicator = "[#FFB86C]●[-]" + } + paddingLen := width - mainTextLen - 1 // 1 for dot, no margin + if paddingLen < 1 { + paddingLen = 1 + } + padding := strings.Repeat(" ", paddingLen) + primary = fmt.Sprintf("%s%s%s", mainText, padding, simplePingIndicator) + default: + // Narrow screen: no ping indicator + primary = mainText + } + } else { + primary = mainText + } + secondary = "" - return + return primary, secondary +} + +// stripSimpleColors removes basic tview color codes for length calculation +func stripSimpleColors(s string) string { + result := s + // Remove color tags like [#FFFFFF] or [-] + for { + start := strings.Index(result, "[") + if start == -1 { + break + } + end := strings.Index(result[start:], "]") + if end == -1 { + break + } + result = result[:start] + result[start+end+1:] + } + return result } func humanizeDuration(t time.Time) string { diff --git a/internal/core/domain/server.go b/internal/core/domain/server.go index c23b301..dfcfb6c 100644 --- a/internal/core/domain/server.go +++ b/internal/core/domain/server.go @@ -27,6 +27,8 @@ type Server struct { LastSeen time.Time PinnedAt time.Time SSHCount int + PingStatus string // "up", "down", "checking", or "" + PingLatency time.Duration // ping latency // Additional SSH config fields // Connection and proxy settings From d6433da21180fee6eaf79d531d5a610c815ba64f Mon Sep 17 00:00:00 2001 From: Pei-Tang Huang Date: Sun, 14 Sep 2025 03:22:21 +0800 Subject: [PATCH 2/3] fix: preserve selected row when updating ping status Maintain the current server selection when ping or ping all operations update the server list. Previously, the selection would reset to the first item after each update. --- internal/adapters/ui/server_list.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/internal/adapters/ui/server_list.go b/internal/adapters/ui/server_list.go index c8cf4d8..8fc22ab 100644 --- a/internal/adapters/ui/server_list.go +++ b/internal/adapters/ui/server_list.go @@ -56,6 +56,13 @@ func (sl *ServerList) build() { } func (sl *ServerList) UpdateServers(servers []domain.Server) { + // Save current selection before clearing + currentIdx := sl.List.GetCurrentItem() + var currentAlias string + if currentIdx >= 0 && currentIdx < len(sl.servers) { + currentAlias = sl.servers[currentIdx].Alias + } + sl.servers = servers sl.List.Clear() @@ -63,6 +70,7 @@ func (sl *ServerList) UpdateServers(servers []domain.Server) { _, _, width, _ := sl.List.GetInnerRect() //nolint:dogsled sl.currentWidth = width + newSelectedIdx := -1 for i := range servers { primary, secondary := formatServerLine(servers[i], width) idx := i @@ -71,12 +79,24 @@ func (sl *ServerList) UpdateServers(servers []domain.Server) { sl.onSelection(sl.servers[idx]) } }) + // Track the new index of previously selected server + if currentAlias != "" && servers[i].Alias == currentAlias { + newSelectedIdx = i + } } if sl.List.GetItemCount() > 0 { - sl.List.SetCurrentItem(0) - if sl.onSelectionChange != nil { - sl.onSelectionChange(sl.servers[0]) + // Restore previous selection if found, otherwise keep first item + if newSelectedIdx >= 0 { + sl.List.SetCurrentItem(newSelectedIdx) + if sl.onSelectionChange != nil { + sl.onSelectionChange(sl.servers[newSelectedIdx]) + } + } else { + sl.List.SetCurrentItem(0) + if sl.onSelectionChange != nil { + sl.onSelectionChange(sl.servers[0]) + } } } } From 636e6af626ad7c65123c36b406f9b84251d0b1d7 Mon Sep 17 00:00:00 2001 From: Pei-Tang Huang Date: Sun, 21 Sep 2025 22:06:22 +0800 Subject: [PATCH 3/3] fix: exclude transient fields from unsaved changes detection After ping status update, editing server form would incorrectly detect unsaved changes. This fix excludes transient runtime fields (PingStatus, PingLatency) and metadata fields from the comparison logic. - Added struct tags to mark transient and metadata fields in domain.Server - Updated serversDiffer() to skip fields based on struct tags - Fixed goconst warnings by using existing status constants - Added comprehensive tests for the change detection logic --- internal/adapters/ui/server_form.go | 24 ++-- internal/adapters/ui/server_form_test.go | 133 +++++++++++++++++++++++ internal/adapters/ui/utils.go | 12 +- internal/core/domain/server.go | 10 +- 4 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 internal/adapters/ui/server_form_test.go diff --git a/internal/adapters/ui/server_form.go b/internal/adapters/ui/server_form.go index 286b47f..497550b 100644 --- a/internal/adapters/ui/server_form.go +++ b/internal/adapters/ui/server_form.go @@ -2039,22 +2039,30 @@ func (sf *ServerForm) serversDiffer(a, b domain.Server) bool { valB := reflect.ValueOf(b) typeA := valA.Type() - // Fields to skip during comparison (lazyssh metadata fields) + // Special fields to skip that don't have tags skipFields := map[string]bool{ - "Aliases": true, // Computed field - "LastSeen": true, // Metadata field - "PinnedAt": true, // Metadata field - "SSHCount": true, // Metadata field + "Aliases": true, // Computed field (derived from Host) } // Iterate through all fields for i := 0; i < valA.NumField(); i++ { fieldA := valA.Field(i) fieldB := valB.Field(i) - fieldName := typeA.Field(i).Name + field := typeA.Field(i) + fieldName := field.Name - // Skip unexported fields and metadata fields - if !fieldA.CanInterface() || skipFields[fieldName] { + // Skip unexported fields + if !fieldA.CanInterface() { + continue + } + + // Skip special fields + if skipFields[fieldName] { + continue + } + + // Check for lazyssh struct tags to skip metadata and transient fields + if tag := field.Tag.Get("lazyssh"); tag == "metadata" || tag == "transient" { continue } diff --git a/internal/adapters/ui/server_form_test.go b/internal/adapters/ui/server_form_test.go new file mode 100644 index 0000000..b7b5ffd --- /dev/null +++ b/internal/adapters/ui/server_form_test.go @@ -0,0 +1,133 @@ +// Copyright 2025. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ui + +import ( + "testing" + "time" + + "github.com/Adembc/lazyssh/internal/core/domain" +) + +func TestServersDifferIgnoresTransientFields(t *testing.T) { + sf := &ServerForm{} + + // Create two servers with identical config but different transient fields + server1 := domain.Server{ + Alias: "test-server", + Host: "example.com", + User: "testuser", + Port: 22, + PingStatus: "up", + PingLatency: 100 * time.Millisecond, + LastSeen: time.Now(), + PinnedAt: time.Now(), + SSHCount: 5, + } + + server2 := domain.Server{ + Alias: "test-server", + Host: "example.com", + User: "testuser", + Port: 22, + PingStatus: "down", // Different transient field + PingLatency: 200 * time.Millisecond, // Different transient field + LastSeen: time.Now().Add(1 * time.Hour), // Different metadata field + PinnedAt: time.Now().Add(2 * time.Hour), // Different metadata field + SSHCount: 10, // Different metadata field + } + + // Should not detect differences since only transient/metadata fields differ + if sf.serversDiffer(server1, server2) { + t.Error("serversDiffer should ignore transient and metadata fields") + } + + // Now change a real config field + server2.Port = 2222 + + // Should detect the difference now + if !sf.serversDiffer(server1, server2) { + t.Error("serversDiffer should detect differences in non-transient fields") + } +} + +func TestServersDifferDetectsRealChanges(t *testing.T) { + sf := &ServerForm{} + + server1 := domain.Server{ + Alias: "test-server", + Host: "example.com", + User: "testuser", + Port: 22, + } + + testCases := []struct { + name string + modify func(*domain.Server) + expect bool + }{ + { + name: "No changes", + modify: func(s *domain.Server) {}, + expect: false, + }, + { + name: "Changed Host", + modify: func(s *domain.Server) { s.Host = "different.com" }, + expect: true, + }, + { + name: "Changed User", + modify: func(s *domain.Server) { s.User = "otheruser" }, + expect: true, + }, + { + name: "Changed Port", + modify: func(s *domain.Server) { s.Port = 2222 }, + expect: true, + }, + { + name: "Added IdentityFile", + modify: func(s *domain.Server) { s.IdentityFiles = []string{"~/.ssh/id_rsa"} }, + expect: true, + }, + { + name: "Changed ProxyJump", + modify: func(s *domain.Server) { s.ProxyJump = "jumphost" }, + expect: true, + }, + { + name: "Changed only PingStatus (transient)", + modify: func(s *domain.Server) { s.PingStatus = "checking" }, + expect: false, + }, + { + name: "Changed only LastSeen (metadata)", + modify: func(s *domain.Server) { s.LastSeen = time.Now().Add(1 * time.Hour) }, + expect: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + server2 := server1 // Copy + tc.modify(&server2) + result := sf.serversDiffer(server1, server2) + if result != tc.expect { + t.Errorf("Expected %v but got %v for test case %s", tc.expect, result, tc.name) + } + }) + } +} diff --git a/internal/adapters/ui/utils.go b/internal/adapters/ui/utils.go index e29acba..5d5a6b2 100644 --- a/internal/adapters/ui/utils.go +++ b/internal/adapters/ui/utils.go @@ -89,7 +89,7 @@ func formatServerLine(s domain.Server, width int) (primary, secondary string) { pingIndicator := "" if s.PingStatus != "" { switch s.PingStatus { - case "up": + case statusUp: if s.PingLatency > 0 { ms := s.PingLatency.Milliseconds() var statusText string @@ -106,9 +106,9 @@ func formatServerLine(s domain.Server, width int) (primary, secondary string) { } else { pingIndicator = "[#4AF626]● UP [-]" } - case "down": + case statusDown: pingIndicator = "[#FF6B6B]● DOWN[-]" - case "checking": + case statusChecking: pingIndicator = "[#FFB86C]● ... [-]" } } @@ -133,11 +133,11 @@ func formatServerLine(s domain.Server, width int) (primary, secondary string) { // Medium screen: show only dot simplePingIndicator := "" switch s.PingStatus { - case "up": + case statusUp: simplePingIndicator = "[#4AF626]●[-]" - case "down": + case statusDown: simplePingIndicator = "[#FF6B6B]●[-]" - case "checking": + case statusChecking: simplePingIndicator = "[#FFB86C]●[-]" } paddingLen := width - mainTextLen - 1 // 1 for dot, no margin diff --git a/internal/core/domain/server.go b/internal/core/domain/server.go index dfcfb6c..a4d508c 100644 --- a/internal/core/domain/server.go +++ b/internal/core/domain/server.go @@ -24,11 +24,11 @@ type Server struct { Port int IdentityFiles []string Tags []string - LastSeen time.Time - PinnedAt time.Time - SSHCount int - PingStatus string // "up", "down", "checking", or "" - PingLatency time.Duration // ping latency + LastSeen time.Time `lazyssh:"metadata"` + PinnedAt time.Time `lazyssh:"metadata"` + SSHCount int `lazyssh:"metadata"` + PingStatus string `lazyssh:"transient"` // "up", "down", "checking", or "" + PingLatency time.Duration `lazyssh:"transient"` // ping latency // Additional SSH config fields // Connection and proxy settings