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

92
tui/Makefile Normal file
View File

@@ -0,0 +1,92 @@
# Makefile for backup-tui
# Ensures consistent binary naming and build process
# Configuration
BINARY_NAME := backup-tui
SOURCE_FILE := main.go
SHELL_DIR := /home/acedanger/shell
TUI_DIR := /home/acedanger/shell/tui
# Go configuration
GOCMD := go
GOBUILD := $(GOCMD) build
GOCLEAN := $(GOCMD) clean
GOTEST := $(GOCMD) test
GOGET := $(GOCMD) get
GOMOD := $(GOCMD) mod
# Default target
.PHONY: all
all: clean deps build install
# Build the application
.PHONY: build
build:
@echo "🔨 Building $(BINARY_NAME)..."
cd $(TUI_DIR) && $(GOBUILD) -o $(BINARY_NAME) $(SOURCE_FILE)
@echo "✅ Build completed: $(TUI_DIR)/$(BINARY_NAME)"
# Download dependencies
.PHONY: deps
deps:
@echo "📦 Downloading dependencies..."
cd $(TUI_DIR) && $(GOMOD) tidy
@echo "✅ Dependencies updated"
# Install to shell directory
.PHONY: install
install: build
@echo "📋 Installing to shell directory..."
cp $(TUI_DIR)/$(BINARY_NAME) $(SHELL_DIR)/$(BINARY_NAME)
chmod +x $(SHELL_DIR)/$(BINARY_NAME)
@echo "✅ Installed to $(SHELL_DIR)/$(BINARY_NAME)"
# Clean build artifacts
.PHONY: clean
clean:
@echo "🧹 Cleaning build artifacts..."
cd $(TUI_DIR) && $(GOCLEAN)
rm -f $(TUI_DIR)/$(BINARY_NAME)
rm -f $(SHELL_DIR)/$(BINARY_NAME)
@echo "✅ Clean completed"
# Run the application
.PHONY: run
run: build
@echo "🚀 Running $(BINARY_NAME)..."
cd $(TUI_DIR) && ./$(BINARY_NAME)
# Test the application
.PHONY: test
test:
@echo "🧪 Running tests..."
cd $(TUI_DIR) && $(GOTEST) -v ./...
# Development build (fast, no install)
.PHONY: dev
dev:
@echo "🔧 Development build..."
cd $(TUI_DIR) && $(GOBUILD) -o $(BINARY_NAME) $(SOURCE_FILE)
@echo "✅ Development build completed"
# Show help
.PHONY: help
help:
@echo "Available targets:"
@echo " all - Clean, download deps, build, and install (default)"
@echo " build - Build the application"
@echo " deps - Download Go dependencies"
@echo " install - Install binary to shell directory"
@echo " clean - Remove build artifacts"
@echo " run - Build and run the application"
@echo " test - Run tests"
@echo " dev - Quick development build"
@echo " help - Show this help message"
# Version info
.PHONY: version
version:
@echo "Binary name: $(BINARY_NAME)"
@echo "Source file: $(SOURCE_FILE)"
@echo "TUI directory: $(TUI_DIR)"
@echo "Shell directory: $(SHELL_DIR)"

View File

@@ -52,7 +52,7 @@ A modern, interactive Terminal User Interface (TUI) for browsing and viewing bac
### 🖥️ User Interface
- **Dual-panel System**: Service list and detailed information viewer
- **Multiple View Modes**: Main browser, logs viewer, status, and configuration views
- **Multiple View Modes**: Main browser, logs viewer, status, and configuration views
- **Tab Navigation**: Switch between panels with Tab key
- **Smart Key Bindings**: Intuitive keyboard shortcuts for navigation
- **Color-coded Display**: Dynamic visual indicators for file types and status
@@ -61,7 +61,7 @@ A modern, interactive Terminal User Interface (TUI) for browsing and viewing bac
### 📦 Backup Service Directories
- **🔵 Plex Backups**: Browse Plex Media Server backup files and archives
- **🖼️ Immich Backups**: View Immich photo management backup files
- **🖼️ Immich Backups**: View Immich photo management backup files
- **🎬 Media Services**: Browse Sonarr, Radarr, Prowlarr, and other media service backups
- **🔧 Environment Files**: View Docker environment and configuration file backups
- **🐳 Docker Configuration**: Browse container and compose file backups
@@ -81,32 +81,47 @@ A modern, interactive Terminal User Interface (TUI) for browsing and viewing bac
### Installation
The TUI is already built and ready to use! Simply run:
The TUI uses a consistent build system with multiple options:
```bash
# From the shell directory
./backup-tui
# Option 1: Use the launcher script (recommended)
cd /home/acedanger/shell
./launch-backup-tui.sh
# Or directly from the tui directory
cd tui && ./backup-manager
# Option 2: Use Makefile for development
cd tui
make all # Build and install
make build # Build only
make dev # Quick development build
# Option 3: Use build script
cd tui
./build.sh
# Option 4: Direct Go build
cd tui
go build -o backup-tui main.go
```
### First Time Setup
1. **Ensure Go 1.19+ is installed** (only needed for rebuilding)
2. **Navigate to your shell directory** where backup scripts are located
3. **Launch the TUI** using `./backup-tui`
3. **Launch the TUI** using `./launch-backup-tui.sh`
4. **Use arrow keys or hjkl** to navigate the interface
5. **Press `?`** for comprehensive help and key bindings
### Launch the TUI
```bash
# From the shell directory
# From the shell directory (recommended - auto-builds if needed)
./launch-backup-tui.sh
# Or if binary exists
./backup-tui
# Or directly from the tui directory
cd tui && ./backup-manager
# Or from the tui directory
cd tui && ./backup-tui
```
### Key Bindings
@@ -203,14 +218,31 @@ The TUI automatically discovers and organizes backup directories:
### Building from Source
The TUI includes multiple build options for consistent binary naming:
```bash
# Option 1: Use Makefile (recommended for development)
cd tui
make all # Clean, build, and install
make build # Build only
make dev # Quick development build
make clean # Remove build artifacts
make help # Show all targets
# Option 2: Use build script
cd tui
./build.sh
# Option 3: Direct Go build
cd tui
export GOROOT=/usr/lib/go-1.19
export PATH=$PATH:$GOROOT/bin
go mod tidy
go build -o backup-manager main.go
go build -o backup-tui main.go
```
**Binary Naming**: All build methods consistently create `backup-tui` binary.
### Dependencies
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - TUI framework
@@ -239,7 +271,7 @@ The TUI provides comprehensive backup file exploration:
### Backup Analysis
- **Size Tracking**: Monitor backup file sizes and storage usage
- **Date Analysis**: Review backup frequency and file modification dates
- **Date Analysis**: Review backup frequency and file modification dates
- **Service Organization**: Files organized by backup service for easy navigation
- **Compression Detection**: Identify compressed archives and their contents
@@ -271,7 +303,7 @@ The TUI complements your existing backup infrastructure:
## 📝 Notes
- The TUI automatically detects backup directory structure and organization
- Provides read-only access to backup files for safe browsing and analysis
- Provides read-only access to backup files for safe browsing and analysis
- Backup files are displayed with metadata including sizes, dates, and compression status
- Log files can be viewed directly within the interface for troubleshooting and analysis

Binary file not shown.

74
tui/build.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
# Build script for backup-tui
# Ensures consistent binary naming across build sessions
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Configuration
BINARY_NAME="backup-tui"
SOURCE_FILE="main.go"
TUI_DIR="/home/acedanger/shell/tui"
SHELL_DIR="/home/acedanger/shell"
# Print colored output
print_color() {
local color="$1"
local message="$2"
case "$color" in
"green") printf "${GREEN}%s${NC}\n" "$message" ;;
"yellow") printf "${YELLOW}%s${NC}\n" "$message" ;;
"red") printf "${RED}%s${NC}\n" "$message" ;;
*) printf "%s\n" "$message" ;;
esac
}
print_color "yellow" "🔨 Building Backup TUI..."
# Change to TUI directory
cd "$TUI_DIR"
# Check if Go is available
if ! command -v go &> /dev/null; then
print_color "red" "❌ Go is not installed or not in PATH"
print_color "yellow" "Please install Go 1.19+ or set up the Go environment"
exit 1
fi
# Ensure dependencies are available
print_color "yellow" "📦 Checking Go dependencies..."
if ! go mod tidy; then
print_color "red" "❌ Failed to download Go dependencies"
exit 1
fi
# Build the binary with consistent naming
print_color "yellow" "🔧 Building ${BINARY_NAME}..."
if go build -o "$BINARY_NAME" "$SOURCE_FILE"; then
print_color "green" "✅ Successfully built ${BINARY_NAME}"
else
print_color "red" "❌ Build failed"
exit 1
fi
# Copy to shell directory for easy access
if cp "$BINARY_NAME" "$SHELL_DIR/$BINARY_NAME"; then
print_color "green" "✅ Copied binary to shell directory"
else
print_color "yellow" "⚠️ Could not copy to shell directory"
fi
# Make executable
chmod +x "$BINARY_NAME"
chmod +x "$SHELL_DIR/$BINARY_NAME" 2>/dev/null || true
print_color "green" "🎉 Build completed successfully!"
print_color "yellow" "Binary location: $TUI_DIR/$BINARY_NAME"
print_color "yellow" "Also available: $SHELL_DIR/$BINARY_NAME"
print_color "yellow" "Launch with: ./backup-tui"

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