package handlers import ( "backea/internal/backup/models" "backea/internal/backup/strategy" "backea/templates" "context" "fmt" "log" "net/http" "os" "sort" "sync" "time" "github.com/labstack/echo/v4" ) type HomepageHandler struct { backupFactory strategy.Factory } func NewHomepageHandler(factory strategy.Factory) *HomepageHandler { return &HomepageHandler{ backupFactory: factory, } } // Home handles the homepage request and displays latest backups by service func (h *HomepageHandler) Home(c echo.Context) error { // Create the data structures serviceBackups := make(map[string]map[string][]models.BackupInfo) serviceConfigs := make(map[string]map[string]templates.ServiceProviderInfo) groupDirectories := make(map[string]string) // Store directories by group name // Get the configuration from the factory factoryConfig := h.getConfig() // Process each service group for groupName, serviceGroup := range factoryConfig.Services { // Initialize maps for this group serviceBackups[groupName] = make(map[string][]models.BackupInfo) serviceConfigs[groupName] = make(map[string]templates.ServiceProviderInfo) // Store the directory at the group level in a separate map groupDirectories[groupName] = serviceGroup.Source.Path // Process each backup config in the group for configIndex, backupConfig := range serviceGroup.BackupConfigs { // Store service configuration serviceConfigs[groupName][configIndex] = templates.ServiceProviderInfo{ Type: backupConfig.BackupStrategy.Type, Provider: backupConfig.BackupStrategy.Provider, Directory: serviceGroup.Source.Path, } } } // Get sorted group names for alphabetical ordering var sortedGroupNames []string for groupName := range serviceBackups { sortedGroupNames = append(sortedGroupNames, groupName) } sort.Strings(sortedGroupNames) // Render the template with sorted group names and directories component := templates.Home(serviceBackups, serviceConfigs, sortedGroupNames, groupDirectories) return component.Render(c.Request().Context(), c.Response().Writer) } // ServiceGroupHeader returns the header section for a specific service group (HTMX endpoint) func (h *HomepageHandler) ServiceGroupHeader(c echo.Context) error { groupName := c.Param("groupName") // Get the configuration factoryConfig := h.getConfig() // Check if the service group exists serviceGroup, exists := factoryConfig.Services[groupName] if !exists { return echo.NewHTTPError(http.StatusNotFound, "Service group not found") } // Create data structures serviceBackups := make(map[string][]models.BackupInfo) serviceConfigs := make(map[string]templates.ServiceProviderInfo) // Setup synchronization var wg sync.WaitGroup var mu sync.Mutex // Process each backup config in the group for configIndex, backupConfig := range serviceGroup.BackupConfigs { // Store service configuration serviceConfigs[configIndex] = templates.ServiceProviderInfo{ Type: backupConfig.BackupStrategy.Type, Provider: backupConfig.BackupStrategy.Provider, Directory: serviceGroup.Source.Path, } // Fetch backups in parallel wg.Add(1) go func(index string) { defer wg.Done() // Get backup strategy strategy, err := h.backupFactory.CreateBackupStrategyForService(groupName, index) if err != nil { log.Printf("Error creating strategy for %s.%s: %v", groupName, index, err) return } // Get backups backups, err := strategy.ListBackups(context.Background(), groupName+"."+index) if err != nil { log.Printf("Error listing backups for %s.%s: %v", groupName, index, err) return } // Sort backups by time (newest first) sort.Slice(backups, func(i, j int) bool { return backups[i].CreationTime.After(backups[j].CreationTime) }) // Store result mu.Lock() serviceBackups[index] = backups mu.Unlock() }(configIndex) } // Wait for all goroutines to finish wg.Wait() // Render just the header component component := templates.GroupHeaderComponent( groupName, serviceBackups, serviceConfigs, serviceGroup.Source.Path, ) return component.Render(c.Request().Context(), c.Response().Writer) } // ServiceGroupBackups returns just the backups table for a specific service group (HTMX endpoint) func (h *HomepageHandler) ServiceGroupBackups(c echo.Context) error { groupName := c.Param("groupName") // Get the configuration factoryConfig := h.getConfig() // Check if the service group exists serviceGroup, exists := factoryConfig.Services[groupName] if !exists { return echo.NewHTTPError(http.StatusNotFound, "Service group not found") } // Create data structures serviceBackups := make(map[string][]models.BackupInfo) serviceConfigs := make(map[string]templates.ServiceProviderInfo) // Setup synchronization var wg sync.WaitGroup var mu sync.Mutex // Process each backup config in the group for configIndex, backupConfig := range serviceGroup.BackupConfigs { // Store service configuration serviceConfigs[configIndex] = templates.ServiceProviderInfo{ Type: backupConfig.BackupStrategy.Type, Provider: backupConfig.BackupStrategy.Provider, Directory: serviceGroup.Source.Path, } // Fetch backups in parallel wg.Add(1) go func(index string) { defer wg.Done() // Get backup strategy strategy, err := h.backupFactory.CreateBackupStrategyForService(groupName, index) if err != nil { log.Printf("Error creating strategy for %s.%s: %v", groupName, index, err) return } // Get backups backups, err := strategy.ListBackups(context.Background(), groupName+"."+index) if err != nil { log.Printf("Error listing backups for %s.%s: %v", groupName, index, err) return } // Sort backups by time (newest first) sort.Slice(backups, func(i, j int) bool { return backups[i].CreationTime.After(backups[j].CreationTime) }) // Store result mu.Lock() serviceBackups[index] = backups mu.Unlock() }(configIndex) } // Wait for all goroutines to finish wg.Wait() // Create a map with just the group for the template groupServiceBackups := make(map[string]map[string][]models.BackupInfo) groupServiceBackups[groupName] = serviceBackups // Create a map with just the group configs for the template groupServiceConfigs := make(map[string]map[string]templates.ServiceProviderInfo) groupServiceConfigs[groupName] = serviceConfigs // Render only the backups table component component := templates.ServiceGroupBackupsTable(groupName, serviceBackups, groupServiceConfigs) return component.Render(c.Request().Context(), c.Response().Writer) } // ServiceGroupAllBackups returns all backups for a specific service group (HTMX endpoint) func (h *HomepageHandler) ServiceGroupAllBackups(c echo.Context) error { groupName := c.Param("groupName") // Get the configuration factoryConfig := h.getConfig() // Check if the service group exists serviceGroup, exists := factoryConfig.Services[groupName] if !exists { return echo.NewHTTPError(http.StatusNotFound, "Service group not found") } // Create data structures serviceBackups := make(map[string][]models.BackupInfo) serviceConfigs := make(map[string]templates.ServiceProviderInfo) // Setup synchronization var wg sync.WaitGroup var mu sync.Mutex // Process each backup config in the group for configIndex, backupConfig := range serviceGroup.BackupConfigs { // Store service configuration serviceConfigs[configIndex] = templates.ServiceProviderInfo{ Type: backupConfig.BackupStrategy.Type, Provider: backupConfig.BackupStrategy.Provider, Directory: serviceGroup.Source.Path, } // Fetch backups in parallel wg.Add(1) go func(index string) { defer wg.Done() // Get backup strategy strategy, err := h.backupFactory.CreateBackupStrategyForService(groupName, index) if err != nil { log.Printf("Error creating strategy for %s.%s: %v", groupName, index, err) return } // Get backups backups, err := strategy.ListBackups(context.Background(), groupName+"."+index) if err != nil { log.Printf("Error listing backups for %s.%s: %v", groupName, index, err) return } // Sort backups by time (newest first) sort.Slice(backups, func(i, j int) bool { return backups[i].CreationTime.After(backups[j].CreationTime) }) // Store result mu.Lock() serviceBackups[index] = backups mu.Unlock() }(configIndex) } // Wait for all goroutines to finish wg.Wait() // Create a map with just the group for the template groupServiceBackups := make(map[string]map[string][]models.BackupInfo) groupServiceBackups[groupName] = serviceBackups // Create a map with just the group configs for the template groupServiceConfigs := make(map[string]map[string]templates.ServiceProviderInfo) groupServiceConfigs[groupName] = serviceConfigs // Also trigger a header update to refresh stats go func() { // Create a new context since the original one might be cancelled ctx := context.Background() // Sleep briefly to ensure the table loads first time.Sleep(100 * time.Millisecond) // Make an HTTP request to refresh the header url := fmt.Sprintf("http://localhost:%s/api/service-group/%s/header", os.Getenv("PORT"), groupName, ) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { log.Printf("Error creating request to refresh header: %v", err) return } // Set HTMX headers to target the correct element req.Header.Set("HX-Request", "true") req.Header.Set("HX-Target", fmt.Sprintf("group-header-%s", groupName)) // Make the request client := &http.Client{} _, err = client.Do(req) if err != nil { log.Printf("Error refreshing header: %v", err) } }() // Render the all backups table component component := templates.ServiceGroupAllBackupsTable(groupName, serviceBackups, groupServiceConfigs) return component.Render(c.Request().Context(), c.Response().Writer) } // Helper method to get configuration from factory func (h *HomepageHandler) getConfig() *models.Configuration { // Type assert to get the concrete FactoryImpl type which has Config field return h.backupFactory.Config }