Files
dbbackup/internal/tui/status.go
Alexander Renz 9c65821250
All checks were successful
CI/CD / Test (push) Successful in 1m14s
CI/CD / Lint (push) Successful in 1m21s
CI/CD / Build & Release (push) Successful in 3m12s
v3.42.9: Fix all timeout bugs and deadlocks
CRITICAL FIXES:
- Encryption detection false positive (IsBackupEncrypted returned true for ALL files)
- 12 cmd.Wait() deadlocks fixed with channel-based context handling
- TUI timeout bugs: 60s->10min for safety checks, 15s->60s for DB listing
- diagnose.go timeouts: 60s->5min for tar/pg_restore operations
- Panic recovery added to parallel backup/restore goroutines
- Variable shadowing fix in restore/engine.go

These bugs caused pg_dump backups to fail through TUI for months.
2026-01-08 05:56:31 +01:00

202 lines
4.6 KiB
Go
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package tui
import (
"context"
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"dbbackup/internal/config"
"dbbackup/internal/database"
"dbbackup/internal/logger"
)
// StatusViewModel shows database status
type StatusViewModel struct {
config *config.Config
logger logger.Logger
parent tea.Model
loading bool
status string
err error
dbCount int
dbVersion string
connected bool
}
func NewStatusView(cfg *config.Config, log logger.Logger, parent tea.Model) StatusViewModel {
return StatusViewModel{
config: cfg,
logger: log,
parent: parent,
loading: true,
status: "Loading status...",
}
}
func (m StatusViewModel) Init() tea.Cmd {
// Auto-forward in auto-confirm mode - skip status check
if m.config.TUIAutoConfirm {
return func() tea.Msg {
return statusAutoQuitMsg{}
}
}
return tea.Batch(
fetchStatus(m.config, m.logger),
tickCmd(),
)
}
// statusAutoQuitMsg triggers automatic quit
type statusAutoQuitMsg struct{}
type tickMsg time.Time
func tickCmd() tea.Cmd {
return tea.Tick(time.Millisecond*100, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
type statusMsg struct {
status string
err error
dbCount int
dbVersion string
connected bool
}
func fetchStatus(cfg *config.Config, log logger.Logger) tea.Cmd {
return func() tea.Msg {
// 30 seconds for status check - slow networks or SSL negotiation
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
dbClient, err := database.New(cfg, log)
if err != nil {
return statusMsg{
status: "",
err: fmt.Errorf("failed to create database client: %w", err),
connected: false,
}
}
defer dbClient.Close()
if err := dbClient.Connect(ctx); err != nil {
return statusMsg{
status: "",
err: fmt.Errorf("connection failed: %w", err),
connected: false,
}
}
version, err := dbClient.GetVersion(ctx)
if err != nil {
log.Warn("failed to get database version", "error", err)
version = "Unknown"
}
databases, err := dbClient.ListDatabases(ctx)
if err != nil {
return statusMsg{
status: "Connected, but failed to list databases",
err: fmt.Errorf("failed to list databases: %w", err),
connected: true,
}
}
return statusMsg{
status: "Database connection successful",
err: nil,
dbCount: len(databases),
dbVersion: version,
connected: true,
}
}
}
func (m StatusViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case statusAutoQuitMsg:
return m.parent, tea.Quit
case tickMsg:
if m.loading {
return m, tickCmd()
}
return m, nil
case statusMsg:
m.loading = false
if msg.status != "" {
m.status = msg.status
}
m.err = msg.err
m.dbCount = msg.dbCount
if msg.dbVersion != "" {
m.dbVersion = msg.dbVersion
}
m.connected = msg.connected
// Auto-forward in auto-confirm mode after status loads
if m.config.TUIAutoConfirm {
return m.parent, tea.Quit
}
return m, nil
case tea.KeyMsg:
if !m.loading {
switch msg.String() {
case "ctrl+c", "q", "esc", "enter":
return m.parent, nil
}
}
}
return m, nil
}
func (m StatusViewModel) View() string {
var s strings.Builder
header := titleStyle.Render("📊 Database Status & Health Check")
s.WriteString(fmt.Sprintf("\n%s\n\n", header))
if m.loading {
spinner := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
frame := int(time.Now().UnixMilli()/100) % len(spinner)
s.WriteString(fmt.Sprintf("%s Loading status information...\n", spinner[frame]))
return s.String()
}
if m.err != nil {
s.WriteString(errorStyle.Render(fmt.Sprintf("❌ Error: %v\n", m.err)))
s.WriteString("\n")
} else {
s.WriteString("Connection Status:\n")
if m.connected {
s.WriteString(successStyle.Render(" ✓ Connected\n"))
} else {
s.WriteString(errorStyle.Render(" ✗ Disconnected\n"))
}
s.WriteString("\n")
s.WriteString(fmt.Sprintf("Database Type: %s (%s)\n", m.config.DisplayDatabaseType(), m.config.DatabaseType))
s.WriteString(fmt.Sprintf("Host: %s:%d\n", m.config.Host, m.config.Port))
s.WriteString(fmt.Sprintf("User: %s\n", m.config.User))
s.WriteString(fmt.Sprintf("Backup Directory: %s\n", m.config.BackupDir))
s.WriteString(fmt.Sprintf("Version: %s\n\n", m.dbVersion))
if m.dbCount > 0 {
s.WriteString(fmt.Sprintf("Databases Found: %s\n", successStyle.Render(fmt.Sprintf("%d", m.dbCount))))
}
s.WriteString("\n")
s.WriteString(successStyle.Render("✓ All systems operational\n"))
}
s.WriteString("\n⌨ Press any key to return to menu\n")
return s.String()
}