feat: add manual file enhancement with development mode support

- Add manual enhance API endpoint (POST /api/enhance?site_id={site}) for triggering file enhancement
- Implement enhance button in JavaScript library status indicator (🔄 Enhance)
- Disable auto-enhancement in development mode to prevent live-reload conflicts
- Add dev mode parameter to SiteManager to control enhancement behavior
- Update API routing structure to support /api/enhance endpoint
- Include enhance button styling and user feedback (loading, success, error states)
- Button triggers file enhancement and page reload to show updated static files

Development workflow improvements:
- Content edits → Immediate editor preview (no unwanted page reloads)
- Manual enhance button → Intentional file updates + reload for testing
- Production mode maintains automatic enhancement on content changes

This resolves the live-reload conflict where automatic file enhancement
was causing unwanted page reloads during content editing in development.
This commit is contained in:
2025-09-10 23:38:46 +02:00
parent 2d0778287d
commit f73e21ce6e
6 changed files with 182 additions and 64 deletions

View File

@@ -104,23 +104,9 @@ When running `insertr serve`, the server automatically:
- **Auto-updates files** when content changes via API - **Auto-updates files** when content changes via API
- **Creates backups** of original files (if enabled) - **Creates backups** of original files (if enabled)
**Development vs Production Behavior:** **Live Enhancement Process:**
**Development Mode** (`--dev-mode`):
- ✅ Content changes saved to database
- ❌ File enhancement **disabled** (prevents live-reload loops)
- ✅ Editor loads content dynamically from API
- ✅ Perfect for `just dev` workflow
**Production Mode** (no `--dev-mode`):
- ✅ Content changes saved to database
- ✅ File enhancement **enabled**
- ✅ Static files updated immediately
- ✅ Changes live instantly
**Live Enhancement Process (Production):**
1. Content updated via API → Database updated 1. Content updated via API → Database updated
2. If site has `auto_enhance: true` AND not in dev mode → File enhancement triggered 2. If site has `auto_enhance: true` → File enhancement triggered
3. Static files updated in-place → Changes immediately live 3. Static files updated in-place → Changes immediately live
### API Endpoints ### API Endpoints

View File

@@ -436,33 +436,14 @@ server:
backup_originals: true backup_originals: true
``` ```
### **Development vs Production Modes**
**Development Mode** (recommended for development):
```bash
# Development: Content updates save to database only
# NO file enhancement to prevent live-reload loops
./insertr serve --dev-mode
just dev # Uses dev mode automatically
```
**Production Mode** (for deployment):
```bash
# Production: Content updates trigger immediate file enhancement
./insertr serve # No --dev-mode flag
```
### **Quick Start** ### **Quick Start**
```bash ```bash
# 1. Configure sites in insertr.yaml # 1. Configure sites in insertr.yaml
# 2. Start development server (no file enhancement) # 2. Start the server
./insertr serve --dev-mode ./insertr serve --dev-mode
# 3. Editor loads content from database dynamically # 3. Your sites are automatically registered and enhanced
# 4. No unwanted file modifications or page reloads during development # 4. Content changes via editor immediately update static files
# 5. For production deployment:
./insertr serve # Enables automatic file enhancement
``` ```
## ⚙️ Configuration ## ⚙️ Configuration

View File

@@ -124,25 +124,30 @@ func runServe(cmd *cobra.Command, args []string) {
router.HandleFunc("/health", api.HealthMiddleware()) router.HandleFunc("/health", api.HealthMiddleware())
// API routes // API routes
apiRouter := router.PathPrefix("/api/content").Subrouter() apiRouter := router.PathPrefix("/api").Subrouter()
// Content endpoints matching the expected API contract // Content endpoints
apiRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET") contentRouter := apiRouter.PathPrefix("/content").Subrouter()
apiRouter.HandleFunc("/{id}", contentHandler.GetContent).Methods("GET") contentRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET")
apiRouter.HandleFunc("/{id}", contentHandler.UpdateContent).Methods("PUT") contentRouter.HandleFunc("/{id}", contentHandler.GetContent).Methods("GET")
apiRouter.HandleFunc("", contentHandler.GetAllContent).Methods("GET") contentRouter.HandleFunc("/{id}", contentHandler.UpdateContent).Methods("PUT")
apiRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST") contentRouter.HandleFunc("", contentHandler.GetAllContent).Methods("GET")
contentRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST")
// Version control endpoints // Version control endpoints
apiRouter.HandleFunc("/{id}/versions", contentHandler.GetContentVersions).Methods("GET") contentRouter.HandleFunc("/{id}/versions", contentHandler.GetContentVersions).Methods("GET")
apiRouter.HandleFunc("/{id}/rollback", contentHandler.RollbackContent).Methods("POST") contentRouter.HandleFunc("/{id}/rollback", contentHandler.RollbackContent).Methods("POST")
// Site enhancement endpoint
apiRouter.HandleFunc("/enhance", contentHandler.EnhanceSite).Methods("POST")
// Handle CORS preflight requests explicitly // Handle CORS preflight requests explicitly
apiRouter.HandleFunc("/{id}", api.CORSPreflightHandler).Methods("OPTIONS") contentRouter.HandleFunc("/{id}", api.CORSPreflightHandler).Methods("OPTIONS")
apiRouter.HandleFunc("", api.CORSPreflightHandler).Methods("OPTIONS") contentRouter.HandleFunc("", api.CORSPreflightHandler).Methods("OPTIONS")
apiRouter.HandleFunc("/bulk", api.CORSPreflightHandler).Methods("OPTIONS") contentRouter.HandleFunc("/bulk", api.CORSPreflightHandler).Methods("OPTIONS")
apiRouter.HandleFunc("/{id}/versions", api.CORSPreflightHandler).Methods("OPTIONS") contentRouter.HandleFunc("/{id}/versions", api.CORSPreflightHandler).Methods("OPTIONS")
apiRouter.HandleFunc("/{id}/rollback", api.CORSPreflightHandler).Methods("OPTIONS") contentRouter.HandleFunc("/{id}/rollback", api.CORSPreflightHandler).Methods("OPTIONS")
apiRouter.HandleFunc("/enhance", api.CORSPreflightHandler).Methods("OPTIONS")
// Start server // Start server
addr := fmt.Sprintf(":%d", port) addr := fmt.Sprintf(":%d", port)

View File

@@ -40,6 +40,53 @@ func (h *ContentHandler) SetSiteManager(siteManager *content.SiteManager) {
h.siteManager = siteManager h.siteManager = siteManager
} }
// EnhanceSite handles POST /api/enhance - manual site enhancement trigger
func (h *ContentHandler) EnhanceSite(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
if h.siteManager == nil {
http.Error(w, "Site manager not available", http.StatusServiceUnavailable)
return
}
// Check if site is registered
site, exists := h.siteManager.GetSite(siteID)
if !exists {
http.Error(w, fmt.Sprintf("Site %s is not registered", siteID), http.StatusNotFound)
return
}
// Perform enhancement
err := h.siteManager.EnhanceSite(siteID)
if err != nil {
log.Printf("❌ Manual enhancement failed for site %s: %v", siteID, err)
http.Error(w, fmt.Sprintf("Enhancement failed: %v", err), http.StatusInternalServerError)
return
}
// Get enhancement statistics
stats := h.siteManager.GetStats()
// Return success response with details
response := map[string]interface{}{
"success": true,
"site_id": siteID,
"site_path": site.Path,
"message": fmt.Sprintf("Successfully enhanced site %s", siteID),
"stats": stats,
"timestamp": time.Now().Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
log.Printf("✅ Manual enhancement completed for site %s", siteID)
}
// GetContent handles GET /api/content/{id} // GetContent handles GET /api/content/{id}
func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) { func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)

View File

@@ -105,23 +105,12 @@ func (sm *SiteManager) GetAllSites() map[string]*SiteConfig {
} }
// IsAutoEnhanceEnabled checks if a site has auto-enhancement enabled // IsAutoEnhanceEnabled checks if a site has auto-enhancement enabled
// Returns false in development mode to prevent unwanted file modifications and live-reload loops
func (sm *SiteManager) IsAutoEnhanceEnabled(siteID string) bool { func (sm *SiteManager) IsAutoEnhanceEnabled(siteID string) bool {
sm.mutex.RLock() // Never auto-enhance in development mode - use manual enhance button instead
defer sm.mutex.RUnlock()
// Disable auto-enhancement in development mode to prevent file modification conflicts with live-reload
if sm.devMode { if sm.devMode {
return false return false
} }
site, exists := sm.sites[siteID]
return exists && site.AutoEnhance
}
// ForceEnhanceEnabled allows testing production behavior in development mode
// This method bypasses the dev_mode check for testing purposes
func (sm *SiteManager) ForceEnhanceEnabled(siteID string) bool {
sm.mutex.RLock() sm.mutex.RLock()
defer sm.mutex.RUnlock() defer sm.mutex.RUnlock()

View File

@@ -330,11 +330,15 @@ export class InsertrAuth {
<span class="insertr-status-text">Visitor Mode</span> <span class="insertr-status-text">Visitor Mode</span>
<span class="insertr-status-dot"></span> <span class="insertr-status-dot"></span>
</div> </div>
<button id="insertr-enhance-btn" class="insertr-enhance-btn" style="display: none;" title="Enhance files with latest content">
🔄 Enhance
</button>
</div> </div>
`; `;
document.body.insertAdjacentHTML('beforeend', statusHtml); document.body.insertAdjacentHTML('beforeend', statusHtml);
this.statusIndicator = document.getElementById('insertr-status'); this.statusIndicator = document.getElementById('insertr-status');
this.setupEnhanceButton();
this.updateStatusIndicator(); this.updateStatusIndicator();
} }
@@ -357,6 +361,9 @@ export class InsertrAuth {
statusText.textContent = 'Authenticated'; statusText.textContent = 'Authenticated';
statusDot.className = 'insertr-status-dot insertr-status-authenticated'; statusDot.className = 'insertr-status-dot insertr-status-authenticated';
} }
// Update enhance button visibility
this.updateEnhanceButtonVisibility();
} }
/** /**
@@ -398,6 +405,28 @@ export class InsertrAuth {
body.insertr-hide-gates .insertr-gate { body.insertr-hide-gates .insertr-gate {
display: none !important; display: none !important;
} }
/* Enhance button styles */
.insertr-enhance-btn {
background: #2563eb;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
margin-left: 8px;
cursor: pointer;
transition: background 0.2s ease;
}
.insertr-enhance-btn:hover {
background: #1d4ed8;
}
.insertr-enhance-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
}
`; `;
const styleSheet = document.createElement('style'); const styleSheet = document.createElement('style');
@@ -556,4 +585,85 @@ export class InsertrAuth {
console.log('✅ OAuth authentication successful'); console.log('✅ OAuth authentication successful');
}, 1000); }, 1000);
} }
/**
* Setup enhance button functionality
*/
setupEnhanceButton() {
const enhanceBtn = document.getElementById('insertr-enhance-btn');
if (!enhanceBtn) return;
enhanceBtn.addEventListener('click', async () => {
await this.enhanceFiles();
});
// Show enhance button only in development/authenticated mode
this.updateEnhanceButtonVisibility();
}
/**
* Update enhance button visibility based on authentication state
*/
updateEnhanceButtonVisibility() {
const enhanceBtn = document.getElementById('insertr-enhance-btn');
if (!enhanceBtn) return;
// Show enhance button when authenticated (indicates dev mode)
if (this.state.isAuthenticated) {
enhanceBtn.style.display = 'inline-block';
} else {
enhanceBtn.style.display = 'none';
}
}
/**
* Trigger manual file enhancement
*/
async enhanceFiles() {
const enhanceBtn = document.getElementById('insertr-enhance-btn');
if (!enhanceBtn) return;
// Get site ID from window context or configuration
const siteId = window.insertrConfig?.siteId || this.options.siteId || 'demo';
try {
// Show loading state
enhanceBtn.textContent = '⏳ Enhancing...';
enhanceBtn.disabled = true;
// Call enhance API
const response = await fetch(`/api/enhance?site_id=${siteId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.state.currentUser?.token || 'mock-token'}`
}
});
if (!response.ok) {
throw new Error(`Enhancement failed: ${response.status} ${response.statusText}`);
}
const result = await response.json();
console.log('✅ Files enhanced successfully:', result);
// Show success state briefly
enhanceBtn.textContent = '✅ Enhanced!';
// Optional: Trigger page reload to show enhanced files
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
console.error('❌ Enhancement failed:', error);
enhanceBtn.textContent = '❌ Failed';
// Reset button after error
setTimeout(() => {
enhanceBtn.textContent = '🔄 Enhance';
enhanceBtn.disabled = false;
}, 2000);
}
}
} }