Files
insertr/cmd/restore.go
Joakim ef1d1083ce fix: systematic element matching bug in enhancement pipeline
- Problem: Element ID collisions between similar elements (logo h1 vs hero h1)
  causing content to be injected into wrong elements
- Root cause: Enhancer used naive tag+class matching instead of parser's
  sophisticated semantic analysis for element identification

Systematic solution:
- Enhanced parser architecture with exported utilities (GetClasses, ContainsClass)
- Added FindElementInDocument() with content-based semantic matching
- Replaced naive findAndInjectNodes() with parser-based element matching
- Removed code duplication between parser and enhancer packages

Backend improvements:
- Moved ID generation to backend for single source of truth
- Added ElementContext struct for frontend-backend communication
- Updated API handlers to support context-based content ID generation

Frontend improvements:
- Enhanced getElementMetadata() to extract semantic context
- Updated save flow to handle both enhanced and non-enhanced elements
- Improved API client to use backend-generated content IDs

Result:
- Unique content IDs: navbar-logo-200530 vs hero-title-a1de7b
- Precise element matching using content validation
- Single source of truth for DOM utilities in parser package
- Eliminated 40+ lines of duplicate code while fixing core bug
2025-09-11 14:14:57 +02:00

164 lines
5.1 KiB
Go

package cmd
import (
"fmt"
"log"
"sort"
"github.com/insertr/insertr/internal/content"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
timestamp string
latest bool
clean bool
)
var restoreCmd = &cobra.Command{
Use: "restore [site-id]",
Short: "Restore a site from backup",
Long: `Restore a registered site from a timestamped backup.
Examples:
insertr restore demo # List available backups
insertr restore demo --clean # Restore from oldest backup (cleanest)
insertr restore demo --latest # Restore from newest backup
insertr restore demo --timestamp 20250910-224704 # Restore from specific backup`,
Args: cobra.ExactArgs(1),
Run: runRestore,
}
func init() {
restoreCmd.Flags().StringVarP(&timestamp, "timestamp", "t", "", "specific backup timestamp to restore from")
restoreCmd.Flags().BoolVar(&latest, "latest", false, "restore from most recent backup")
restoreCmd.Flags().BoolVar(&clean, "clean", false, "restore from oldest backup (cleanest state)")
// Bind flags to viper
viper.BindPFlag("restore.timestamp", restoreCmd.Flags().Lookup("timestamp"))
viper.BindPFlag("restore.latest", restoreCmd.Flags().Lookup("latest"))
viper.BindPFlag("restore.clean", restoreCmd.Flags().Lookup("clean"))
}
func runRestore(cmd *cobra.Command, args []string) {
siteID := args[0]
// Initialize content client (we don't actually need it for restore, but SiteManager expects it)
contentClient := content.NewMockClient()
// Initialize site manager
siteManager := content.NewSiteManager(contentClient, "./insertr-backups", false)
// Load sites from configuration to register them
if siteConfigs := viper.Get("server.sites"); siteConfigs != nil {
if configs, ok := siteConfigs.([]interface{}); ok {
var sites []*content.SiteConfig
for _, configInterface := range configs {
if configMap, ok := configInterface.(map[string]interface{}); ok {
site := &content.SiteConfig{}
if id, ok := configMap["site_id"].(string); ok {
site.SiteID = id
}
if path, ok := configMap["path"].(string); ok {
site.Path = path
}
if domain, ok := configMap["domain"].(string); ok {
site.Domain = domain
}
if autoEnhance, ok := configMap["auto_enhance"].(bool); ok {
site.AutoEnhance = autoEnhance
}
if backupOriginals, ok := configMap["backup_originals"].(bool); ok {
site.BackupOriginals = backupOriginals
}
sites = append(sites, site)
}
}
if err := siteManager.RegisterSites(sites); err != nil {
log.Fatalf("Failed to register sites: %v", err)
}
}
}
// List available backups
backups, err := siteManager.ListBackups(siteID)
if err != nil {
log.Fatalf("Failed to list backups: %v", err)
}
if len(backups) == 0 {
fmt.Printf("❌ No backups found for site '%s'\n", siteID)
fmt.Printf("💡 Backups are created automatically during enhancement when backup_originals is enabled\n")
return
}
// Sort backups chronologically
sort.Strings(backups)
// Handle different restore modes
var targetTimestamp string
if timestamp != "" {
// Specific timestamp provided
targetTimestamp = timestamp
found := false
for _, backup := range backups {
if backup == targetTimestamp {
found = true
break
}
}
if !found {
fmt.Printf("❌ Backup timestamp '%s' not found for site '%s'\n", targetTimestamp, siteID)
fmt.Printf("📋 Available backups:\n")
for i, backup := range backups {
if i == 0 {
fmt.Printf(" %s (oldest/cleanest)\n", backup)
} else if i == len(backups)-1 {
fmt.Printf(" %s (newest)\n", backup)
} else {
fmt.Printf(" %s\n", backup)
}
}
return
}
} else if clean {
// Restore from oldest backup (cleanest)
targetTimestamp = backups[0]
fmt.Printf("🧹 Restoring from oldest backup (cleanest state): %s\n", targetTimestamp)
} else if latest {
// Restore from newest backup
targetTimestamp = backups[len(backups)-1]
fmt.Printf("🔄 Restoring from newest backup: %s\n", targetTimestamp)
} else {
// No specific option - list available backups
fmt.Printf("📋 Available backups for site '%s':\n", siteID)
for i, backup := range backups {
if i == 0 {
fmt.Printf(" %s (oldest/cleanest) ← use --clean\n", backup)
} else if i == len(backups)-1 {
fmt.Printf(" %s (newest) ← use --latest\n", backup)
} else {
fmt.Printf(" %s\n", backup)
}
}
fmt.Printf("\nUsage:\n")
fmt.Printf(" insertr restore %s --clean # restore from oldest backup\n", siteID)
fmt.Printf(" insertr restore %s --latest # restore from newest backup\n", siteID)
fmt.Printf(" insertr restore %s --timestamp %s # restore from specific backup\n", siteID, backups[0])
return
}
// Perform restore
fmt.Printf("🔄 Restoring site '%s' from backup %s...\n", siteID, targetTimestamp)
if err := siteManager.RestoreFromBackup(siteID, targetTimestamp); err != nil {
log.Fatalf("❌ Restore failed: %v", err)
}
fmt.Printf("✅ Successfully restored site '%s' from backup %s\n", siteID, targetTimestamp)
fmt.Printf("💡 Site files have been restored to their state from %s\n", targetTimestamp)
}