feat: Implement backup TUI with enhanced refresh functionality and consistent build system

This commit is contained in:
Peter Wood
2025-06-04 08:57:09 -04:00
parent 780e78f132
commit 8b514ac0b2
8 changed files with 508 additions and 60 deletions

View File

@@ -209,7 +209,8 @@ type Model struct {
logs []LogEntry
shellPath string
items []BackupItem // Store backup items for reference
currentView string // "main", "logs", "status"
currentView string // "main", "logs", "status", "directory"
currentDirPath string // Path of currently viewed directory for refresh
}
// Initialize the model
@@ -351,7 +352,7 @@ func (d BackupItemDelegate) Render(w io.Writer, m list.Model, index int, listIte
// Helper function to convert list items to BackupItems
func convertToBackupItems(items []list.Item) []BackupItem {
backupItems := make([]BackupItem, len(items))
for i, item := range items {
for i, item := range items {
if bi, ok := item.(BackupItem); ok {
backupItems[i] = bi
}
@@ -404,8 +405,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keys.Refresh):
// Refresh current view
if len(m.items) > 0 && m.currentView == "main" {
m.showItemDescription()
switch m.currentView {
case "main":
if len(m.items) > 0 {
m.showItemDescription()
}
case "directory":
// Reload the current directory
if m.currentDirPath != "" {
cmds = append(cmds, m.loadBackupDirectory(m.currentDirPath))
}
case "logs":
// Reload log files
cmds = append(cmds, m.loadLogFiles())
}
case key.Matches(msg, m.keys.ViewLogs):
@@ -423,12 +435,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keys.Clear):
m.viewport.SetContent("Output cleared.\n\nSelect a backup directory from the list and press Enter to browse.")
m.currentView = "main"
m.currentDirPath = "" // Clear directory path
// Show description for currently selected item
m.showItemDescription()
case key.Matches(msg, m.keys.Escape) && m.currentView != "main":
m.viewport.SetContent("Welcome to Backup File Browser!\n\nSelect a backup directory from the list and press Enter to browse.\n\nUse 'v' to view logs, 's' for status, and 'tab' to switch between panels.")
m.currentView = "main"
m.currentDirPath = "" // Clear directory path
// Show description for currently selected item
m.showItemDescription()
@@ -436,6 +450,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if i, ok := m.list.SelectedItem().(BackupItem); ok {
switch i.itemType {
case "directory":
// Store the directory path for refresh functionality
m.currentDirPath = i.path
// Load backup directory details
cmds = append(cmds, m.loadBackupDirectory(i.path))
case "logs":
@@ -607,7 +623,7 @@ func (m *Model) showStatus() {
content.WriteString("=== BACKUP DIRECTORY STATUS ===\n\n")
content.WriteString("📂 Available Backup Directories:\n\n")
for _, item := range m.items {
if item.itemType == "directory" {
content.WriteString(fmt.Sprintf("🔧 Service: %s\n", strings.ToUpper(item.service)))
@@ -684,13 +700,13 @@ func (m *Model) showItemDescription() {
}
item := m.items[selectedIndex]
var content strings.Builder
content.WriteString(fmt.Sprintf("📋 %s\n", item.title))
content.WriteString(strings.Repeat("=", len(item.title)+3) + "\n\n")
content.WriteString(fmt.Sprintf("📝 Description:\n%s\n\n", item.description))
// Add detailed information based on service type
switch item.service {
case "plex":
@@ -699,82 +715,82 @@ func (m *Model) showItemDescription() {
content.WriteString("• Configuration and preferences\n")
content.WriteString("• Plugin and custom data\n")
content.WriteString("• Compressed archive files (.tar.gz)\n\n")
content.WriteString("📁 Typical Contents:\n")
content.WriteString("• Library database files\n")
content.WriteString("• User preferences and settings\n")
content.WriteString("• Custom artwork and thumbnails\n")
content.WriteString("• Plugin configurations\n\n")
case "immich":
content.WriteString("📸 Immich Photo Management Backups:\n")
content.WriteString("• PostgreSQL database dumps\n")
content.WriteString("• Uploaded photos and videos\n")
content.WriteString("• User settings and albums\n")
content.WriteString("• Machine learning models\n\n")
content.WriteString("📁 Typical Contents:\n")
content.WriteString("• Database backup files (.sql)\n")
content.WriteString("• Media file archives\n")
content.WriteString("• Configuration backups\n")
content.WriteString("• Thumbnail caches\n\n")
case "sonarr":
content.WriteString("📺 Sonarr TV Series Management:\n")
content.WriteString("• Series tracking database\n")
content.WriteString("• Download client configurations\n")
content.WriteString("• Quality profiles and settings\n")
content.WriteString("• Custom scripts and metadata\n\n")
case "radarr":
content.WriteString("🎬 Radarr Movie Management:\n")
content.WriteString("• Movie collection database\n")
content.WriteString("• Indexer and download settings\n")
content.WriteString("• Quality and format preferences\n")
content.WriteString("• Custom filters and lists\n\n")
case "prowlarr":
content.WriteString("🔍 Prowlarr Indexer Management:\n")
content.WriteString("• Indexer configurations\n")
content.WriteString("• API keys and credentials\n")
content.WriteString("• Sync profiles and settings\n")
content.WriteString("• Application mappings\n\n")
case "jellyseerr":
content.WriteString("🎯 Jellyseerr Request Management:\n")
content.WriteString("• User request history\n")
content.WriteString("• Notification configurations\n")
content.WriteString("• Integration settings\n")
content.WriteString("• Custom user permissions\n\n")
case "sabnzbd":
content.WriteString("📥 SABnzbd Download Client:\n")
content.WriteString("• Server and category configs\n")
content.WriteString("• Download queue history\n")
content.WriteString("• Post-processing scripts\n")
content.WriteString("• User settings and passwords\n\n")
case "tautulli":
content.WriteString("📊 Tautulli Plex Statistics:\n")
content.WriteString("• View history and statistics\n")
content.WriteString("• User activity tracking\n")
content.WriteString("• Notification configurations\n")
content.WriteString("• Custom dashboard settings\n\n")
case "audiobookshelf":
content.WriteString("📚 Audiobookshelf Server:\n")
content.WriteString("• Library and user data\n")
content.WriteString("• Listening progress tracking\n")
content.WriteString("• Playlist and collection info\n")
content.WriteString("• Server configuration\n\n")
case "docker-data":
content.WriteString("🐳 Docker Container Data:\n")
content.WriteString("• Volume and bind mount backups\n")
content.WriteString("• Container persistent data\n")
content.WriteString("• Application state files\n")
content.WriteString("• Configuration volumes\n\n")
case "logs":
content.WriteString("📋 Backup Operation Logs:\n")
content.WriteString("• Daily backup operation logs\n")
@@ -782,7 +798,7 @@ func (m *Model) showItemDescription() {
content.WriteString("• Backup timing and performance\n")
content.WriteString("• System health monitoring\n\n")
}
// Add usage instructions based on item type
content.WriteString("\n🚀 Actions:\n")
switch item.itemType {
@@ -797,7 +813,7 @@ func (m *Model) showItemDescription() {
content.WriteString("• View backup operation details\n")
content.WriteString("• Check for errors or warnings\n")
}
content.WriteString("\n💡 Navigation:\n")
content.WriteString("• Tab: Switch between panels\n")
content.WriteString("• r: Refresh current view\n")
@@ -815,19 +831,19 @@ func (m *Model) showBackupDetails(details BackupDetails) {
var content strings.Builder
content.WriteString(fmt.Sprintf("📁 %s Backup Directory\n", details.Directory.Name))
content.WriteString(strings.Repeat("=", len(details.Directory.Name)+20) + "\n\n")
// Directory summary
content.WriteString("📊 Directory Summary:\n")
content.WriteString(fmt.Sprintf(" 📁 Path: %s\n", details.Directory.Path))
// Show if this is using a scheduled subfolder
if strings.HasSuffix(details.Directory.Path, "/scheduled") {
content.WriteString(" 📅 Source: Scheduled backups (from 'scheduled' subfolder)\n")
}
content.WriteString(fmt.Sprintf(" 📄 Files: %d\n", details.TotalFiles))
content.WriteString(fmt.Sprintf(" 💾 Total Size: %s\n", formatSize(details.TotalSize)))
if !details.NewestBackup.IsZero() {
content.WriteString(fmt.Sprintf(" 🕒 Newest: %s\n", details.NewestBackup.Format("2006-01-02 15:04:05")))
}
@@ -835,35 +851,35 @@ func (m *Model) showBackupDetails(details BackupDetails) {
content.WriteString(fmt.Sprintf(" 📅 Oldest: %s\n", details.OldestBackup.Format("2006-01-02 15:04:05")))
}
content.WriteString("\n")
// File listings
if len(details.Files) > 0 {
content.WriteString("📄 Backup Files:\n")
content.WriteString(" Name Size Modified\n")
content.WriteString(" " + strings.Repeat("-", 50) + "\n")
for _, file := range details.Files {
// Truncate filename if too long
displayName := file.Name
if len(displayName) > 25 {
displayName = displayName[:22] + "..."
}
compressionIndicator := ""
if file.IsCompressed {
compressionIndicator = "📦 "
}
content.WriteString(fmt.Sprintf(" %s%-25s %8s %s\n",
content.WriteString(fmt.Sprintf(" %s%-25s %8s %s\n",
compressionIndicator,
displayName,
formatSize(file.Size),
displayName,
formatSize(file.Size),
file.ModifiedTime.Format("2006-01-02 15:04")))
}
} else {
content.WriteString("📂 No backup files found in this directory.\n")
}
content.WriteString("\n💡 Navigation:\n")
content.WriteString(" • x: Return to main view\n")
content.WriteString(" • r: Refresh directory listing\n")
@@ -877,7 +893,7 @@ func (m *Model) showLogFiles(logFiles []LogFile) {
var content strings.Builder
content.WriteString("📋 Backup Operation Logs\n")
content.WriteString("=========================\n\n")
if len(logFiles) == 0 {
content.WriteString("📂 No log files found.\n")
content.WriteString("Log files are typically stored in /mnt/share/media/backups/logs/\n")
@@ -885,27 +901,27 @@ func (m *Model) showLogFiles(logFiles []LogFile) {
content.WriteString("📄 Available Log Files:\n")
content.WriteString(" Filename Size Date\n")
content.WriteString(" " + strings.Repeat("-", 45) + "\n")
for _, logFile := range logFiles {
displayName := logFile.Name
if len(displayName) > 25 {
displayName = displayName[:22] + "..."
}
dateStr := "Unknown"
if !logFile.Date.IsZero() {
dateStr = logFile.Date.Format("2006-01-02 15:04")
}
content.WriteString(fmt.Sprintf(" %-25s %8s %s\n",
displayName,
formatSize(logFile.Size),
content.WriteString(fmt.Sprintf(" %-25s %8s %s\n",
displayName,
formatSize(logFile.Size),
dateStr))
}
content.WriteString(fmt.Sprintf("\n📊 Total: %d log files\n", len(logFiles)))
}
content.WriteString("\n💡 Navigation:\n")
content.WriteString(" • Enter: View log content (when a specific log is selected)\n")
content.WriteString(" • x: Return to main view\n")
@@ -934,11 +950,11 @@ func (m *Model) loadBackupDirectory(dirPath string) tea.Cmd {
// Check if there's a "scheduled" subfolder - if so, use that instead
scheduledPath := filepath.Join(dirPath, "scheduled")
actualPath := dirPath
if info, err := os.Stat(scheduledPath); err == nil && info.IsDir() {
actualPath = scheduledPath
}
files, err := os.ReadDir(actualPath)
if err != nil {
return errorMsg{error: fmt.Sprintf("Failed to read directory %s: %v", actualPath, err)}
@@ -1035,7 +1051,7 @@ func (m *Model) loadLogFiles() tea.Cmd {
if strings.HasPrefix(name, "backup_log_") && strings.HasSuffix(name, ".md") {
datePart := strings.TrimPrefix(name, "backup_log_")
datePart = strings.TrimSuffix(datePart, ".md")
if len(datePart) >= 15 { // YYYYMMDD_HHMMSS
if parsedDate, err := time.Parse("20060102_150405", datePart); err == nil {
logDate = parsedDate