Compare commits
2 Commits
5136484cd2
...
34951221d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34951221d3 | ||
|
|
68939cdc08 |
@@ -27,7 +27,7 @@
|
|||||||
{"id":"wg-admin-gp4","title":"Create Go TUI epic","description":"Epic: Convert wg-client-manager bash script to a modern, responsive Go TUI application using Bubble Tea framework. Provides better UX with interactive forms, real-time status updates, and intuitive keyboard navigation.","status":"closed","priority":1,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.286393088+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T18:06:58.972856627+01:00","closed_at":"2026-01-12T18:06:58.972856627+01:00","close_reason":"All Go TUI tasks completed across 6 phases:\n\nPhase 1 (Foundation): Project init, config system, TUI skeleton\nPhase 2 (Client List): Parsing, table, real-time status \nPhase 3 (Add Client): Form, key generation, config files\nPhase 4 (Detail/Delete): Detail view, delete functionality, QR codes\nPhase 5 (UX): Search, help screen, color themes\nPhase 6 (Backup/Restore): Backup operations, restore functionality\n\nImplementation details:\n- Total packages added: bubbletea, lipgloss, bubbles, huh, qrterminal\n- Files created: 25+ Go source files\n- All features implemented: CRUD clients, status checking, QR codes, themes, search, backup/restore\n- Build successful with ~6MB binary\n- All tasks closed and synced to remote\n\nApplication ready for testing and deployment."}
|
{"id":"wg-admin-gp4","title":"Create Go TUI epic","description":"Epic: Convert wg-client-manager bash script to a modern, responsive Go TUI application using Bubble Tea framework. Provides better UX with interactive forms, real-time status updates, and intuitive keyboard navigation.","status":"closed","priority":1,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.286393088+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T18:06:58.972856627+01:00","closed_at":"2026-01-12T18:06:58.972856627+01:00","close_reason":"All Go TUI tasks completed across 6 phases:\n\nPhase 1 (Foundation): Project init, config system, TUI skeleton\nPhase 2 (Client List): Parsing, table, real-time status \nPhase 3 (Add Client): Form, key generation, config files\nPhase 4 (Detail/Delete): Detail view, delete functionality, QR codes\nPhase 5 (UX): Search, help screen, color themes\nPhase 6 (Backup/Restore): Backup operations, restore functionality\n\nImplementation details:\n- Total packages added: bubbletea, lipgloss, bubbles, huh, qrterminal\n- Files created: 25+ Go source files\n- All features implemented: CRUD clients, status checking, QR codes, themes, search, backup/restore\n- Build successful with ~6MB binary\n- All tasks closed and synced to remote\n\nApplication ready for testing and deployment."}
|
||||||
{"id":"wg-admin-gw9","title":"Add search and filter clients","description":"Implement client search functionality with keyboard shortcut (/). Allow filtering by client name, IP address, or status. Highlight matching results in real-time as user types.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.285733479+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T18:06:07.246176563+01:00","closed_at":"2026-01-12T18:06:07.246176563+01:00","close_reason":"Search component implemented in internal/tui/components/search.go. Real-time filtering by name, IP, or status with '/' activation. Filter types cycle with Tab. Match count display. Integrated with list screen to dynamically update client table.","dependencies":[{"issue_id":"wg-admin-gw9","depends_on_id":"wg-admin-xum","type":"blocks","created_at":"2026-01-12T17:04:36.200521151+01:00","created_by":"Calmcacil"}]}
|
{"id":"wg-admin-gw9","title":"Add search and filter clients","description":"Implement client search functionality with keyboard shortcut (/). Allow filtering by client name, IP address, or status. Highlight matching results in real-time as user types.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.285733479+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T18:06:07.246176563+01:00","closed_at":"2026-01-12T18:06:07.246176563+01:00","close_reason":"Search component implemented in internal/tui/components/search.go. Real-time filtering by name, IP, or status with '/' activation. Filter types cycle with Tab. Match count display. Integrated with list screen to dynamically update client table.","dependencies":[{"issue_id":"wg-admin-gw9","depends_on_id":"wg-admin-xum","type":"blocks","created_at":"2026-01-12T17:04:36.200521151+01:00","created_by":"Calmcacil"}]}
|
||||||
{"id":"wg-admin-hd4","title":"Add keyboard shortcuts help","description":"Create help screen displaying all keyboard shortcuts. Show on '?' key press or in status bar. Include shortcuts for navigation (j/k, arrows), actions (a=add, d=delete, q=quit), and help (?).","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.283054325+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T17:48:32.156575557+01:00","closed_at":"2026-01-12T17:48:32.156575557+01:00","close_reason":"Help screen implemented with keyboard shortcuts table organized by category. Shows on ? key or status bar. Navigation, actions, and other shortcuts documented with lipgloss styling.","dependencies":[{"issue_id":"wg-admin-hd4","depends_on_id":"wg-admin-xum","type":"blocks","created_at":"2026-01-12T17:04:53.117669255+01:00","created_by":"Calmcacil"}]}
|
{"id":"wg-admin-hd4","title":"Add keyboard shortcuts help","description":"Create help screen displaying all keyboard shortcuts. Show on '?' key press or in status bar. Include shortcuts for navigation (j/k, arrows), actions (a=add, d=delete, q=quit), and help (?).","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.283054325+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T17:48:32.156575557+01:00","closed_at":"2026-01-12T17:48:32.156575557+01:00","close_reason":"Help screen implemented with keyboard shortcuts table organized by category. Shows on ? key or status bar. Navigation, actions, and other shortcuts documented with lipgloss styling.","dependencies":[{"issue_id":"wg-admin-hd4","depends_on_id":"wg-admin-xum","type":"blocks","created_at":"2026-01-12T17:04:53.117669255+01:00","created_by":"Calmcacil"}]}
|
||||||
{"id":"wg-admin-he6","title":"Reduce status refresh interval from 10 to 3 seconds for better real-time awareness","description":"Currently connection status refreshes every 10 seconds which is too slow for real-time awareness. Reduce interval to 3 seconds and add 'Last updated: X ago' indicator so users know when data was last refreshed.","status":"in_progress","priority":1,"issue_type":"bug","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:04.503637878+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T22:58:59.044822026+01:00"}
|
{"id":"wg-admin-he6","title":"Reduce status refresh interval from 10 to 3 seconds for better real-time awareness","description":"Currently connection status refreshes every 10 seconds which is too slow for real-time awareness. Reduce interval to 3 seconds and add 'Last updated: X ago' indicator so users know when data was last refreshed.","status":"closed","priority":1,"issue_type":"bug","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:04.503637878+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T23:04:52.82485488+01:00","closed_at":"2026-01-12T23:04:52.82485488+01:00","close_reason":"Completed: Reduced refresh interval from 10 to 3 seconds and added 'Last updated' indicator showing time since last refresh"}
|
||||||
{"id":"wg-admin-hln","title":"Improve empty state messages with actionable guidance and call-to-action","description":"When no clients exist or search returns no results, provide clear guidance on next steps. Add tips like 'Press [a] to add your first client', 'Check spelling', 'Try shorter search term'. Make empty states welcoming and helpful.","status":"closed","priority":1,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:04.504693809+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T23:03:04.93789438+01:00","closed_at":"2026-01-12T23:03:04.93789438+01:00","close_reason":"Implemented improved empty state messages with actionable guidance including tips on adding clients, checking spelling, and shorter search terms"}
|
{"id":"wg-admin-hln","title":"Improve empty state messages with actionable guidance and call-to-action","description":"When no clients exist or search returns no results, provide clear guidance on next steps. Add tips like 'Press [a] to add your first client', 'Check spelling', 'Try shorter search term'. Make empty states welcoming and helpful.","status":"closed","priority":1,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:04.504693809+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T23:03:04.93789438+01:00","closed_at":"2026-01-12T23:03:04.93789438+01:00","close_reason":"Implemented improved empty state messages with actionable guidance including tips on adding clients, checking spelling, and shorter search terms"}
|
||||||
{"id":"wg-admin-hzl","title":"Add visual status icons (● Online/● Offline) for better scanning","description":"Connection status is currently just text ('Connected'/'Disconnected'). Add visual indicators with colored circles/icons for faster scanning. Green ● for connected, red ● for disconnected. This improves visual hierarchy and scanability.","status":"in_progress","priority":1,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:04.554974884+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T22:59:07.349352101+01:00"}
|
{"id":"wg-admin-hzl","title":"Add visual status icons (● Online/● Offline) for better scanning","description":"Connection status is currently just text ('Connected'/'Disconnected'). Add visual indicators with colored circles/icons for faster scanning. Green ● for connected, red ● for disconnected. This improves visual hierarchy and scanability.","status":"in_progress","priority":1,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:04.554974884+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T22:59:07.349352101+01:00"}
|
||||||
{"id":"wg-admin-iqs","title":"Add navigation breadcrumbs to all screens for context","description":"Currently no breadcrumbs or navigation context in screens. Users can't tell where they are in the navigation hierarchy (list \u003e details, add \u003e form). Add breadcrumb at top of each screen showing navigation path.","status":"in_progress","priority":2,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:23.210874178+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T22:59:07.202238466+01:00"}
|
{"id":"wg-admin-iqs","title":"Add navigation breadcrumbs to all screens for context","description":"Currently no breadcrumbs or navigation context in screens. Users can't tell where they are in the navigation hierarchy (list \u003e details, add \u003e form). Add breadcrumb at top of each screen showing navigation path.","status":"in_progress","priority":2,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:23.210874178+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T22:59:07.202238466+01:00"}
|
||||||
|
|||||||
@@ -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