mirror of
https://github.com/acedanger/shell.git
synced 2025-12-06 01:10:12 -08:00
feat: Implement backup TUI with enhanced refresh functionality and consistent build system
This commit is contained in:
108
tui/main.go
108
tui/main.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user