Reduce status refresh interval to 3 seconds and add last updated indicator
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user