Reduce status refresh interval to 3 seconds and add last updated indicator

This commit is contained in:
Calmcacil
2026-01-12 23:04:48 +01:00
parent 5136484cd2
commit 68939cdc08
6 changed files with 56 additions and 41 deletions

View File

@@ -99,6 +99,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
m.currentScreen = screens.NewListScreen() m.currentScreen = screens.NewListScreen()
return m, m.currentScreen.Init() return m, m.currentScreen.Init()
case screens.ErrMsg:
// An error occurred - show error screen
m.previousScreen = m.currentScreen
m.errorScreen = screens.NewErrorScreen(msg.Err)
m.currentScreen = m.errorScreen
return m, m.currentScreen.Init()
} }
// Pass messages to current screen // Pass messages to current screen

View File

@@ -29,7 +29,7 @@ var (
addHelpStyle = lipgloss.NewStyle(). addHelpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")). Foreground(lipgloss.Color("241")).
MarginTop(1) MarginTop(1)
loadingStyle = lipgloss.NewStyle(). addLoadingStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("62")). Foreground(lipgloss.Color("62")).
Bold(true). Bold(true).
MarginTop(1) MarginTop(1)
@@ -147,7 +147,7 @@ func (s *AddScreen) View() string {
} }
if s.isCreating { if s.isCreating {
return loadingStyle.Render( return addLoadingStyle.Render(
lipgloss.JoinVertical( lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
addTitleStyle.Render("Add New WireGuard Client"), addTitleStyle.Render("Add New WireGuard Client"),
@@ -172,7 +172,7 @@ func (s *AddScreen) createClient(name, dns string, usePSK bool) tea.Cmd {
// Create the client via wireguard package // Create the client via wireguard package
err := wireguard.CreateClient(name, dns, usePSK) err := wireguard.CreateClient(name, dns, usePSK)
if err != nil { if err != nil {
return errMsg{err: fmt.Errorf("failed to create client: %w", err)} return ErrMsg{Err: fmt.Errorf("failed to create client: %w", err)}
} }
// Return success message // Return success message

View File

@@ -26,11 +26,8 @@ type DetailScreen struct {
// Styles // Styles
var ( var (
detailTitleStyle = lipgloss.NewStyle().Bold(true).MarginTop(0)
detailSectionStyle = lipgloss.NewStyle().Bold(true).MarginTop(1)
detailLabelStyle = lipgloss.NewStyle().Width(18)
detailValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255")) detailValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
dimmedContentStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) dimmedContentStyle = theme.StyleMuted
) )
// NewDetailScreen creates a new detail screen for a client // NewDetailScreen creates a new detail screen for a client
@@ -118,9 +115,7 @@ func (s *DetailScreen) View() string {
if s.showConfig && s.configDisplay != nil { if s.showConfig && s.configDisplay != nil {
// Render underlying content dimmed // Render underlying content dimmed
content := s.renderContent() content := s.renderContent()
dimmedContent := lipgloss.NewStyle(). dimmedContent := dimmedContentStyle.Render(content)
Foreground(lipgloss.Color("244")).
Render(content)
// Overlay config display modal // Overlay config display modal
return lipgloss.JoinVertical( return lipgloss.JoinVertical(
@@ -134,9 +129,7 @@ func (s *DetailScreen) View() string {
if s.showConfirm && s.confirmModal != nil { if s.showConfirm && s.confirmModal != nil {
// Render underlying content dimmed // Render underlying content dimmed
content := s.renderContent() content := s.renderContent()
dimmedContent := lipgloss.NewStyle(). dimmedContent := dimmedContentStyle.Render(content)
Foreground(lipgloss.Color("244")).
Render(content)
// Overlay confirmation modal // Overlay confirmation modal
return lipgloss.JoinVertical( return lipgloss.JoinVertical(
@@ -153,21 +146,21 @@ func (s *DetailScreen) View() string {
func (s *DetailScreen) renderContent() string { func (s *DetailScreen) renderContent() string {
statusText := s.status statusText := s.status
if s.status == wireguard.StatusConnected { if s.status == wireguard.StatusConnected {
statusText = detailConnectedStyle.Render("● " + s.status) statusText = theme.StyleSuccess.Bold(true).Render("● " + s.status)
} else { } else {
statusText = detailDisconnectedStyle.Render("● " + s.status) statusText = theme.StyleError.Bold(true).Render("● " + s.status)
} }
// Build content // Build content
content := lipgloss.JoinVertical( content := lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
detailTitleStyle.Render(fmt.Sprintf("Client Details: %s", s.client.Name)), theme.StyleTitle.Render(fmt.Sprintf("Client Details: %s", s.client.Name)),
"", "",
s.renderField("Status", statusText), s.renderField("Status", statusText),
s.renderField("IPv4 Address", detailValueStyle.Render(s.client.IPv4)), s.renderField("IPv4 Address", detailValueStyle.Render(s.client.IPv4)),
s.renderField("IPv6 Address", detailValueStyle.Render(s.client.IPv6)), s.renderField("IPv6 Address", detailValueStyle.Render(s.client.IPv6)),
"", "",
detailSectionStyle.Render("WireGuard Configuration"), theme.StyleSubtitle.Bold(true).MarginTop(1).Render("WireGuard Configuration"),
s.renderField("Public Key", detailValueStyle.Render(s.client.PublicKey)), s.renderField("Public Key", detailValueStyle.Render(s.client.PublicKey)),
s.renderField("Preshared Key", detailValueStyle.Render(func() string { s.renderField("Preshared Key", detailValueStyle.Render(func() string {
if s.client.HasPSK { if s.client.HasPSK {
@@ -176,7 +169,7 @@ func (s *DetailScreen) renderContent() string {
return "Not configured" return "Not configured"
}())), }())),
"", "",
detailSectionStyle.Render("Connection Info"), theme.StyleSubtitle.Bold(true).MarginTop(1).Render("Connection Info"),
s.renderField("Last Handshake", detailValueStyle.Render(s.formatHandshake())), s.renderField("Last Handshake", detailValueStyle.Render(s.formatHandshake())),
s.renderField("Transfer (Rx/Tx)", detailValueStyle.Render(fmt.Sprintf("%s / %s", s.transferRx, s.transferTx))), s.renderField("Transfer (Rx/Tx)", detailValueStyle.Render(fmt.Sprintf("%s / %s", s.transferRx, s.transferTx))),
s.renderField("Config Path", detailValueStyle.Render(s.client.ConfigPath)), s.renderField("Config Path", detailValueStyle.Render(s.client.ConfigPath)),
@@ -184,7 +177,7 @@ func (s *DetailScreen) renderContent() string {
) )
// Add help text // Add help text
helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] View Config • [q/b] Back") helpText := theme.StyleHelpKey.MarginTop(1).Render("Actions: [d] Delete • [c] View Config • [q/b] Back")
content = lipgloss.JoinVertical(lipgloss.Left, content, helpText) content = lipgloss.JoinVertical(lipgloss.Left, content, helpText)
return content return content
@@ -193,7 +186,7 @@ func (s *DetailScreen) renderContent() string {
// renderField renders a label-value pair // renderField renders a label-value pair
func (s *DetailScreen) renderField(label string, value string) string { func (s *DetailScreen) renderField(label string, value string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, return lipgloss.JoinHorizontal(lipgloss.Left,
detailLabelStyle.Render(label), theme.StyleSubtitle.Width(18).Render(label),
value, value,
) )
} }
@@ -221,7 +214,7 @@ func (s *DetailScreen) formatHandshake() string {
func (s *DetailScreen) loadClientStatus() tea.Msg { func (s *DetailScreen) loadClientStatus() tea.Msg {
peers, err := wireguard.GetAllPeers() peers, err := wireguard.GetAllPeers()
if err != nil { if err != nil {
return errMsg{err: err} return ErrMsg{Err: err}
} }
// Find peer by public key // Find peer by public key
@@ -250,7 +243,7 @@ func (s *DetailScreen) loadConfig() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
config, err := wireguard.GetClientConfigContent(s.client.Name) config, err := wireguard.GetClientConfigContent(s.client.Name)
if err != nil { if err != nil {
return errMsg{fmt.Errorf("failed to load client config: %w", err)} return ErrMsg{Err: fmt.Errorf("failed to load client config: %w", err)}
} }
// Create or update config display modal // Create or update config display modal
@@ -269,7 +262,7 @@ func (s *DetailScreen) deleteClient() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
err := wireguard.DeleteClient(s.client.Name) err := wireguard.DeleteClient(s.client.Name)
if err != nil { if err != nil {
return errMsg{fmt.Errorf("failed to delete client: %w", err)} return ErrMsg{fmt.Errorf("failed to delete client: %w", err)}
} }
return ClientDeletedMsg{ return ClientDeletedMsg{
Name: s.client.Name, Name: s.client.Name,

View File

@@ -137,9 +137,13 @@ func (s *ListScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
// View renders the list screen // View renders the list screen
func (s *ListScreen) View() string { func (s *ListScreen) View() string {
// Breadcrumb: Home
breadcrumb := components.RenderBreadcrumb([]components.BreadcrumbItem{
{Label: "Clients", ID: "list"},
})
if len(s.clients) == 0 { if len(s.clients) == 0 {
// Empty state with helpful guidance // Empty state with helpful guidance
return s.search.View() + "\n\n" + return breadcrumb + "\n" + s.search.View() + "\n\n" +
lipgloss.NewStyle(). lipgloss.NewStyle().
Foreground(lipgloss.Color("226")). Foreground(lipgloss.Color("226")).
Bold(true). Bold(true).
@@ -164,7 +168,7 @@ func (s *ListScreen) View() string {
// Check if there are no matches // Check if there are no matches
if s.search.IsActive() && len(s.filtered) == 0 && s.search.GetQuery() != "" { if s.search.IsActive() && len(s.filtered) == 0 && s.search.GetQuery() != "" {
// Empty search results with helpful tips // Empty search results with helpful tips
return s.search.View() + "\n\n" + return breadcrumb + "\n" + s.search.View() + "\n\n" +
lipgloss.NewStyle(). lipgloss.NewStyle().
Foreground(lipgloss.Color("226")). Foreground(lipgloss.Color("226")).
Bold(true). Bold(true).
@@ -200,7 +204,7 @@ func (s *ListScreen) View() string {
Foreground(lipgloss.Color("241")). Foreground(lipgloss.Color("241")).
Render("Last updated: " + timeAgo) Render("Last updated: " + timeAgo)
return s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText return breadcrumb + "\n" + s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText
} }
// formatDuration returns a human-readable string for the duration // formatDuration returns a human-readable string for the duration
@@ -218,7 +222,7 @@ func formatDuration(d time.Duration) string {
func (s *ListScreen) loadClients() tea.Msg { func (s *ListScreen) loadClients() tea.Msg {
clients, err := wireguard.ListClients() clients, err := wireguard.ListClients()
if err != nil { if err != nil {
return errMsg{err: err} return ErrMsg{Err: err}
} }
// Get status for each client // Get status for each client

View File

@@ -54,8 +54,8 @@ func (s *QRScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
case configLoadedMsg: case configLoadedMsg:
s.configContent = msg.content s.configContent = msg.content
s.generateQRCode() s.generateQRCode()
case errMsg: case ErrMsg:
s.errorMsg = msg.err.Error() s.errorMsg = msg.Err.Error()
} }
return s, nil return s, nil
@@ -76,7 +76,7 @@ func (s *QRScreen) View() string {
func (s *QRScreen) loadConfig() tea.Msg { func (s *QRScreen) loadConfig() tea.Msg {
content, err := wireguard.GetClientConfigContent(s.clientName) content, err := wireguard.GetClientConfigContent(s.clientName)
if err != nil { if err != nil {
return errMsg{err: err} return ErrMsg{Err: err}
} }
return configLoadedMsg{content: content} return configLoadedMsg{content: content}
} }

View File

@@ -43,15 +43,21 @@ var (
restoreInfoStyle = lipgloss.NewStyle(). restoreInfoStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")). Foreground(lipgloss.Color("241")).
MarginTop(1) MarginTop(1)
loadingStyle = lipgloss.NewStyle(). restoreLoadingStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("62")). Foreground(lipgloss.Color("62")).
Bold(true) Bold(true)
) )
// NewRestoreScreen creates a new restore screen // NewRestoreScreen creates a new restore screen
func NewRestoreScreen() *RestoreScreen { func NewRestoreScreen() *RestoreScreen {
// Create spinner for loading states
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("62"))
return &RestoreScreen{ return &RestoreScreen{
showConfirm: false, showConfirm: false,
spinner: s,
} }
} }
@@ -64,6 +70,12 @@ func (s *RestoreScreen) Init() tea.Cmd {
func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
// If restoring, only update spinner
if s.isRestoring && !s.showConfirm {
s.spinner, cmd = s.spinner.Update(msg)
return s, cmd
}
// Handle confirmation modal // Handle confirmation modal
if s.showConfirm && s.confirmModal != nil { if s.showConfirm && s.confirmModal != nil {
_, cmd = s.confirmModal.Update(msg) _, cmd = s.confirmModal.Update(msg)
@@ -74,7 +86,7 @@ func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
// User confirmed restore // User confirmed restore
s.isRestoring = true s.isRestoring = true
s.showConfirm = false s.showConfirm = false
return s, s.performRestore() return s, tea.Sequence(s.spinner.Tick, s.performRestore())
} }
// User cancelled - close modal // User cancelled - close modal
s.showConfirm = false s.showConfirm = false
@@ -87,7 +99,7 @@ func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
if msg.String() == "enter" && s.confirmModal.IsConfirmed() && s.selectedBackup != nil { if msg.String() == "enter" && s.confirmModal.IsConfirmed() && s.selectedBackup != nil {
s.isRestoring = true s.isRestoring = true
s.showConfirm = false s.showConfirm = false
return s, s.performRestore() return s, tea.Sequence(s.spinner.Tick, s.performRestore())
} }
} }
@@ -130,14 +142,14 @@ func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
} }
} }
} }
case restoreCompletedMsg: case RestoreCompletedMsg:
s.isRestoring = false s.isRestoring = false
if msg.err != nil { if msg.Err != nil {
s.restoreError = msg.err s.restoreError = msg.Err
s.message = fmt.Sprintf("Restore failed: %v", msg.err) s.message = fmt.Sprintf("Restore failed: %v", msg.Err)
} else { } else {
s.restoreSuccess = true s.restoreSuccess = true
s.message = fmt.Sprintf("Restore successful! Safety backup created at: %s", msg.safetyBackupPath) s.message = fmt.Sprintf("Restore successful! Safety backup created at: %s", msg.SafetyBackupPath)
} }
} }
@@ -227,7 +239,7 @@ func (s *RestoreScreen) renderContent() string {
func (s *RestoreScreen) loadBackups() tea.Msg { func (s *RestoreScreen) loadBackups() tea.Msg {
backups, err := backup.ListBackups() backups, err := backup.ListBackups()
if err != nil { if err != nil {
return errMsg{err: err} return ErrMsg{Err: err}
} }
return backupsLoadedMsg{backups: backups} return backupsLoadedMsg{backups: backups}
} }