- Move all ContentItem, ContentClient, ContentResponse types to engine/types.go as single source of truth - Remove duplicate type definitions from content/types.go - Update all imports across codebase to use engine types - Enhance engine to extract existing data-content-id from HTML markup - Simplify frontend to always send html_markup, let server handle ID extraction/generation - Fix contentId reference errors in frontend error handling - Add getAttribute helper method to engine for ID extraction - Add GetAllContent method to engine.DatabaseClient - Update enhancer to use engine.ContentClient interface - All builds and API endpoints verified working This resolves the 400 Bad Request errors and creates a unified architecture where the server is the single source of truth for all ID generation and content type management.
310 lines
7.6 KiB
Go
310 lines
7.6 KiB
Go
package content
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/insertr/insertr/internal/engine"
|
|
)
|
|
|
|
// SiteConfig represents configuration for a registered site
|
|
type SiteConfig struct {
|
|
SiteID string `yaml:"site_id"`
|
|
Path string `yaml:"path"`
|
|
Domain string `yaml:"domain,omitempty"`
|
|
AutoEnhance bool `yaml:"auto_enhance"`
|
|
BackupOriginals bool `yaml:"backup_originals"`
|
|
}
|
|
|
|
// SiteManager handles registration and enhancement of static sites
|
|
type SiteManager struct {
|
|
sites map[string]*SiteConfig
|
|
enhancer *Enhancer
|
|
mutex sync.RWMutex
|
|
backupDir string
|
|
devMode bool
|
|
}
|
|
|
|
// NewSiteManager creates a new site manager
|
|
func NewSiteManager(contentClient engine.ContentClient, backupDir string, devMode bool) *SiteManager {
|
|
if backupDir == "" {
|
|
backupDir = "./insertr-backups"
|
|
}
|
|
|
|
return &SiteManager{
|
|
sites: make(map[string]*SiteConfig),
|
|
enhancer: NewEnhancer(contentClient, ""), // siteID will be set per operation
|
|
backupDir: backupDir,
|
|
devMode: devMode,
|
|
}
|
|
}
|
|
|
|
// RegisterSite adds a site to the manager
|
|
func (sm *SiteManager) RegisterSite(config *SiteConfig) error {
|
|
sm.mutex.Lock()
|
|
defer sm.mutex.Unlock()
|
|
|
|
// Validate site configuration
|
|
if config.SiteID == "" {
|
|
return fmt.Errorf("site_id is required")
|
|
}
|
|
if config.Path == "" {
|
|
return fmt.Errorf("path is required for site %s", config.SiteID)
|
|
}
|
|
|
|
// Check if path exists
|
|
if _, err := os.Stat(config.Path); os.IsNotExist(err) {
|
|
return fmt.Errorf("site path does not exist: %s", config.Path)
|
|
}
|
|
|
|
// Convert to absolute path
|
|
absPath, err := filepath.Abs(config.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve absolute path for %s: %w", config.Path, err)
|
|
}
|
|
config.Path = absPath
|
|
|
|
sm.sites[config.SiteID] = config
|
|
log.Printf("📁 Registered site %s at %s", config.SiteID, config.Path)
|
|
|
|
return nil
|
|
}
|
|
|
|
// RegisterSites bulk registers multiple sites from configuration
|
|
func (sm *SiteManager) RegisterSites(configs []*SiteConfig) error {
|
|
for _, config := range configs {
|
|
if err := sm.RegisterSite(config); err != nil {
|
|
return fmt.Errorf("failed to register site %s: %w", config.SiteID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetSite returns a registered site configuration
|
|
func (sm *SiteManager) GetSite(siteID string) (*SiteConfig, bool) {
|
|
sm.mutex.RLock()
|
|
defer sm.mutex.RUnlock()
|
|
|
|
site, exists := sm.sites[siteID]
|
|
return site, exists
|
|
}
|
|
|
|
// GetAllSites returns all registered sites
|
|
func (sm *SiteManager) GetAllSites() map[string]*SiteConfig {
|
|
sm.mutex.RLock()
|
|
defer sm.mutex.RUnlock()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make(map[string]*SiteConfig)
|
|
for id, site := range sm.sites {
|
|
result[id] = site
|
|
}
|
|
return result
|
|
}
|
|
|
|
// IsAutoEnhanceEnabled checks if a site has auto-enhancement enabled
|
|
func (sm *SiteManager) IsAutoEnhanceEnabled(siteID string) bool {
|
|
// Never auto-enhance in development mode - use manual enhance button instead
|
|
if sm.devMode {
|
|
return false
|
|
}
|
|
|
|
sm.mutex.RLock()
|
|
defer sm.mutex.RUnlock()
|
|
|
|
site, exists := sm.sites[siteID]
|
|
return exists && site.AutoEnhance
|
|
}
|
|
|
|
// EnhanceSite performs in-place enhancement of a registered site
|
|
func (sm *SiteManager) EnhanceSite(siteID string) error {
|
|
sm.mutex.RLock()
|
|
site, exists := sm.sites[siteID]
|
|
sm.mutex.RUnlock()
|
|
|
|
if !exists {
|
|
return fmt.Errorf("site %s is not registered", siteID)
|
|
}
|
|
|
|
log.Printf("🔄 Enhancing site %s at %s", siteID, site.Path)
|
|
|
|
// Create backup if enabled
|
|
if site.BackupOriginals {
|
|
if err := sm.createBackup(siteID, site.Path); err != nil {
|
|
log.Printf("⚠️ Failed to create backup for site %s: %v", siteID, err)
|
|
// Continue with enhancement even if backup fails
|
|
}
|
|
}
|
|
|
|
// Perform in-place enhancement
|
|
if err := sm.enhancer.EnhanceInPlace(site.Path, siteID); err != nil {
|
|
return fmt.Errorf("failed to enhance site %s: %w", siteID, err)
|
|
}
|
|
|
|
log.Printf("✅ Successfully enhanced site %s", siteID)
|
|
return nil
|
|
}
|
|
|
|
// EnhanceAllSites enhances all registered sites that have auto-enhancement enabled
|
|
func (sm *SiteManager) EnhanceAllSites() error {
|
|
sm.mutex.RLock()
|
|
sites := make([]*SiteConfig, 0, len(sm.sites))
|
|
for _, site := range sm.sites {
|
|
if site.AutoEnhance {
|
|
sites = append(sites, site)
|
|
}
|
|
}
|
|
sm.mutex.RUnlock()
|
|
|
|
var errors []error
|
|
for _, site := range sites {
|
|
if err := sm.EnhanceSite(site.SiteID); err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("enhancement failed for some sites: %v", errors)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createBackup creates a timestamped backup of the site
|
|
func (sm *SiteManager) createBackup(siteID, sitePath string) error {
|
|
// Create backup directory structure
|
|
timestamp := time.Now().Format("20060102-150405")
|
|
backupPath := filepath.Join(sm.backupDir, siteID, timestamp)
|
|
|
|
if err := os.MkdirAll(backupPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
// Copy HTML files to backup
|
|
return filepath.Walk(sitePath, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Only backup HTML files
|
|
if !info.IsDir() && filepath.Ext(path) == ".html" {
|
|
relPath, err := filepath.Rel(sitePath, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
backupFilePath := filepath.Join(backupPath, relPath)
|
|
|
|
// Create directory structure in backup
|
|
if err := os.MkdirAll(filepath.Dir(backupFilePath), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy file
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(backupFilePath, content, info.Mode())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// RestoreFromBackup restores a site from a specific backup
|
|
func (sm *SiteManager) RestoreFromBackup(siteID, timestamp string) error {
|
|
sm.mutex.RLock()
|
|
site, exists := sm.sites[siteID]
|
|
sm.mutex.RUnlock()
|
|
|
|
if !exists {
|
|
return fmt.Errorf("site %s is not registered", siteID)
|
|
}
|
|
|
|
backupPath := filepath.Join(sm.backupDir, siteID, timestamp)
|
|
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("backup not found: %s", backupPath)
|
|
}
|
|
|
|
log.Printf("🔄 Restoring site %s from backup %s", siteID, timestamp)
|
|
|
|
// Copy backup files back to site directory
|
|
return filepath.Walk(backupPath, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
relPath, err := filepath.Rel(backupPath, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
targetPath := filepath.Join(site.Path, relPath)
|
|
|
|
// Create directory structure
|
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy file
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(targetPath, content, info.Mode())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ListBackups returns available backups for a site
|
|
func (sm *SiteManager) ListBackups(siteID string) ([]string, error) {
|
|
backupSitePath := filepath.Join(sm.backupDir, siteID)
|
|
|
|
if _, err := os.Stat(backupSitePath); os.IsNotExist(err) {
|
|
return []string{}, nil // No backups exist
|
|
}
|
|
|
|
entries, err := os.ReadDir(backupSitePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read backup directory: %w", err)
|
|
}
|
|
|
|
var backups []string
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
backups = append(backups, entry.Name())
|
|
}
|
|
}
|
|
|
|
return backups, nil
|
|
}
|
|
|
|
// GetStats returns statistics about registered sites
|
|
func (sm *SiteManager) GetStats() map[string]interface{} {
|
|
sm.mutex.RLock()
|
|
defer sm.mutex.RUnlock()
|
|
|
|
autoEnhanceCount := 0
|
|
for _, site := range sm.sites {
|
|
if site.AutoEnhance {
|
|
autoEnhanceCount++
|
|
}
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"total_sites": len(sm.sites),
|
|
"auto_enhance_sites": autoEnhanceCount,
|
|
"backup_directory": sm.backupDir,
|
|
}
|
|
}
|