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:
18
COMMANDS.md
18
COMMANDS.md
@@ -104,23 +104,9 @@ When running `insertr serve`, the server automatically:
|
||||
- **Auto-updates files** when content changes via API
|
||||
- **Creates backups** of original files (if enabled)
|
||||
|
||||
**Development vs Production Behavior:**
|
||||
|
||||
**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):**
|
||||
**Live Enhancement Process:**
|
||||
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
|
||||
|
||||
### API Endpoints
|
||||
|
||||
25
README.md
25
README.md
@@ -436,33 +436,14 @@ server:
|
||||
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**
|
||||
```bash
|
||||
# 1. Configure sites in insertr.yaml
|
||||
# 2. Start development server (no file enhancement)
|
||||
# 2. Start the server
|
||||
./insertr serve --dev-mode
|
||||
|
||||
# 3. Editor loads content from database dynamically
|
||||
# 4. No unwanted file modifications or page reloads during development
|
||||
|
||||
# 5. For production deployment:
|
||||
./insertr serve # Enables automatic file enhancement
|
||||
# 3. Your sites are automatically registered and enhanced
|
||||
# 4. Content changes via editor immediately update static files
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
33
cmd/serve.go
33
cmd/serve.go
@@ -124,25 +124,30 @@ func runServe(cmd *cobra.Command, args []string) {
|
||||
router.HandleFunc("/health", api.HealthMiddleware())
|
||||
|
||||
// API routes
|
||||
apiRouter := router.PathPrefix("/api/content").Subrouter()
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
|
||||
// Content endpoints matching the expected API contract
|
||||
apiRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET")
|
||||
apiRouter.HandleFunc("/{id}", contentHandler.GetContent).Methods("GET")
|
||||
apiRouter.HandleFunc("/{id}", contentHandler.UpdateContent).Methods("PUT")
|
||||
apiRouter.HandleFunc("", contentHandler.GetAllContent).Methods("GET")
|
||||
apiRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST")
|
||||
// Content endpoints
|
||||
contentRouter := apiRouter.PathPrefix("/content").Subrouter()
|
||||
contentRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET")
|
||||
contentRouter.HandleFunc("/{id}", contentHandler.GetContent).Methods("GET")
|
||||
contentRouter.HandleFunc("/{id}", contentHandler.UpdateContent).Methods("PUT")
|
||||
contentRouter.HandleFunc("", contentHandler.GetAllContent).Methods("GET")
|
||||
contentRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST")
|
||||
|
||||
// Version control endpoints
|
||||
apiRouter.HandleFunc("/{id}/versions", contentHandler.GetContentVersions).Methods("GET")
|
||||
apiRouter.HandleFunc("/{id}/rollback", contentHandler.RollbackContent).Methods("POST")
|
||||
contentRouter.HandleFunc("/{id}/versions", contentHandler.GetContentVersions).Methods("GET")
|
||||
contentRouter.HandleFunc("/{id}/rollback", contentHandler.RollbackContent).Methods("POST")
|
||||
|
||||
// Site enhancement endpoint
|
||||
apiRouter.HandleFunc("/enhance", contentHandler.EnhanceSite).Methods("POST")
|
||||
|
||||
// Handle CORS preflight requests explicitly
|
||||
apiRouter.HandleFunc("/{id}", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
apiRouter.HandleFunc("", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
apiRouter.HandleFunc("/bulk", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
apiRouter.HandleFunc("/{id}/versions", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
apiRouter.HandleFunc("/{id}/rollback", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
contentRouter.HandleFunc("/{id}", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
contentRouter.HandleFunc("", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
contentRouter.HandleFunc("/bulk", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
contentRouter.HandleFunc("/{id}/versions", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
contentRouter.HandleFunc("/{id}/rollback", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
apiRouter.HandleFunc("/enhance", api.CORSPreflightHandler).Methods("OPTIONS")
|
||||
|
||||
// Start server
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
|
||||
@@ -40,6 +40,53 @@ func (h *ContentHandler) SetSiteManager(siteManager *content.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}
|
||||
func (h *ContentHandler) GetContent(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -105,23 +105,12 @@ func (sm *SiteManager) GetAllSites() map[string]*SiteConfig {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
// Disable auto-enhancement in development mode to prevent file modification conflicts with live-reload
|
||||
// Never auto-enhance in development mode - use manual enhance button instead
|
||||
if sm.devMode {
|
||||
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()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
|
||||
@@ -330,11 +330,15 @@ export class InsertrAuth {
|
||||
<span class="insertr-status-text">Visitor Mode</span>
|
||||
<span class="insertr-status-dot"></span>
|
||||
</div>
|
||||
<button id="insertr-enhance-btn" class="insertr-enhance-btn" style="display: none;" title="Enhance files with latest content">
|
||||
🔄 Enhance
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', statusHtml);
|
||||
this.statusIndicator = document.getElementById('insertr-status');
|
||||
this.setupEnhanceButton();
|
||||
this.updateStatusIndicator();
|
||||
}
|
||||
|
||||
@@ -357,6 +361,9 @@ export class InsertrAuth {
|
||||
statusText.textContent = '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 {
|
||||
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');
|
||||
@@ -556,4 +585,85 @@ export class InsertrAuth {
|
||||
console.log('✅ OAuth authentication successful');
|
||||
}, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user