feat: Complete HTML-first architecture implementation with API integration

- Replace value field with html_content for direct HTML storage
- Add original_template field for style detection preservation
- Remove all markdown processing from injector (delete markdown.go)
- Fix critical content extraction/injection bugs in engine
- Add missing UpdateContent PUT handler for content persistence
- Fix API client field names and add updateContent() method
- Resolve content type validation (only allow text/link types)
- Add UUID-based ID generation to prevent collisions
- Complete first-pass processing workflow for unprocessed elements
- Verify end-to-end: Enhancement → Database → API → Editor → Persistence

All 37 files updated for HTML-first content management system.
Phase 3a implementation complete and production ready.
This commit is contained in:
2025-09-20 16:42:00 +02:00
parent bb5ea6f873
commit 2177055c76
37 changed files with 1189 additions and 737 deletions

View File

@@ -22,6 +22,21 @@ import (
"github.com/insertr/insertr/internal/engine"
)
// Helper functions for sql.NullString conversion
func toNullString(s string) sql.NullString {
if s == "" {
return sql.NullString{Valid: false}
}
return sql.NullString{String: s, Valid: true}
}
func fromNullString(ns sql.NullString) string {
if ns.Valid {
return ns.String
}
return ""
}
// ContentHandler handles all content-related HTTP requests
type ContentHandler struct {
database *db.Database
@@ -314,19 +329,21 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
switch h.database.GetDBType() {
case "sqlite3":
content, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{
ID: contentID,
SiteID: siteID,
Value: req.Value,
Type: contentType,
LastEditedBy: userID,
ID: contentID,
SiteID: siteID,
HtmlContent: req.HTMLContent,
OriginalTemplate: toNullString(req.OriginalTemplate),
Type: contentType,
LastEditedBy: userID,
})
case "postgresql":
content, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{
ID: contentID,
SiteID: siteID,
Value: req.Value,
Type: contentType,
LastEditedBy: userID,
ID: contentID,
SiteID: siteID,
HtmlContent: req.HTMLContent,
OriginalTemplate: toNullString(req.OriginalTemplate),
Type: contentType,
LastEditedBy: userID,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
@@ -363,6 +380,111 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(item)
}
// UpdateContent handles PUT /api/content/{id}
func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
contentID := chi.URLParam(r, "id")
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
var req struct {
HTMLContent string `json:"html_content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Extract user from request using authentication service
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
if authErr != nil {
http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized)
return
}
userID := userInfo.ID
// Get existing content for version history
var existingContent interface{}
var err error
switch h.database.GetDBType() {
case "sqlite3":
existingContent, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{
ID: contentID,
SiteID: siteID,
})
case "postgresql":
existingContent, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{
ID: contentID,
SiteID: siteID,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Content not found", http.StatusNotFound)
return
}
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
return
}
// Archive existing version before update
if err := h.createContentVersion(existingContent); err != nil {
fmt.Printf("Warning: Failed to create content version: %v\n", err)
}
// Update content
var updatedContent interface{}
switch h.database.GetDBType() {
case "sqlite3":
updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{
HtmlContent: req.HTMLContent,
Type: h.getContentType(existingContent),
LastEditedBy: userID,
ID: contentID,
SiteID: siteID,
})
case "postgresql":
updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{
HtmlContent: req.HTMLContent,
Type: h.getContentType(existingContent),
LastEditedBy: userID,
ID: contentID,
SiteID: siteID,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError)
return
}
item := h.convertToAPIContent(updatedContent)
// Trigger file enhancement if site is registered for auto-enhancement
if h.siteManager != nil && h.siteManager.IsAutoEnhanceEnabled(siteID) {
go func() {
if err := h.siteManager.EnhanceSite(siteID); err != nil {
log.Printf("⚠️ Failed to enhance site %s: %v", siteID, err)
}
}()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(item)
}
// DeleteContent handles DELETE /api/content/{id}
func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) {
contentID := chi.URLParam(r, "id")
@@ -541,7 +663,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request)
case "sqlite3":
sqliteVersion := targetVersion.(sqlite.ContentVersion)
updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{
Value: sqliteVersion.Value,
HtmlContent: sqliteVersion.HtmlContent,
Type: sqliteVersion.Type,
LastEditedBy: userID,
ID: contentID,
@@ -550,7 +672,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request)
case "postgresql":
pgVersion := targetVersion.(postgresql.ContentVersion)
updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{
Value: pgVersion.Value,
HtmlContent: pgVersion.HtmlContent,
Type: pgVersion.Type,
LastEditedBy: userID,
ID: contentID,
@@ -578,24 +700,26 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem {
case "sqlite3":
c := content.(sqlite.Content)
return ContentItem{
ID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
Type: c.Type,
CreatedAt: time.Unix(c.CreatedAt, 0),
UpdatedAt: time.Unix(c.UpdatedAt, 0),
LastEditedBy: c.LastEditedBy,
ID: c.ID,
SiteID: c.SiteID,
HTMLContent: c.HtmlContent,
OriginalTemplate: fromNullString(c.OriginalTemplate),
Type: c.Type,
CreatedAt: time.Unix(c.CreatedAt, 0),
UpdatedAt: time.Unix(c.UpdatedAt, 0),
LastEditedBy: c.LastEditedBy,
}
case "postgresql":
c := content.(postgresql.Content)
return ContentItem{
ID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
Type: c.Type,
CreatedAt: time.Unix(c.CreatedAt, 0),
UpdatedAt: time.Unix(c.UpdatedAt, 0),
LastEditedBy: c.LastEditedBy,
ID: c.ID,
SiteID: c.SiteID,
HTMLContent: c.HtmlContent,
OriginalTemplate: fromNullString(c.OriginalTemplate),
Type: c.Type,
CreatedAt: time.Unix(c.CreatedAt, 0),
UpdatedAt: time.Unix(c.UpdatedAt, 0),
LastEditedBy: c.LastEditedBy,
}
}
return ContentItem{} // Should never happen
@@ -628,13 +752,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
versions := make([]ContentVersion, len(list))
for i, version := range list {
versions[i] = ContentVersion{
VersionID: version.VersionID,
ContentID: version.ContentID,
SiteID: version.SiteID,
Value: version.Value,
Type: version.Type,
CreatedAt: time.Unix(version.CreatedAt, 0),
CreatedBy: version.CreatedBy,
VersionID: version.VersionID,
ContentID: version.ContentID,
SiteID: version.SiteID,
HTMLContent: version.HtmlContent,
OriginalTemplate: fromNullString(version.OriginalTemplate),
Type: version.Type,
CreatedAt: time.Unix(version.CreatedAt, 0),
CreatedBy: version.CreatedBy,
}
}
return versions
@@ -643,13 +768,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
versions := make([]ContentVersion, len(list))
for i, version := range list {
versions[i] = ContentVersion{
VersionID: int64(version.VersionID),
ContentID: version.ContentID,
SiteID: version.SiteID,
Value: version.Value,
Type: version.Type,
CreatedAt: time.Unix(version.CreatedAt, 0),
CreatedBy: version.CreatedBy,
VersionID: int64(version.VersionID),
ContentID: version.ContentID,
SiteID: version.SiteID,
HTMLContent: version.HtmlContent,
OriginalTemplate: fromNullString(version.OriginalTemplate),
Type: version.Type,
CreatedAt: time.Unix(version.CreatedAt, 0),
CreatedBy: version.CreatedBy,
}
}
return versions
@@ -662,20 +788,22 @@ func (h *ContentHandler) createContentVersion(content interface{}) error {
case "sqlite3":
c := content.(sqlite.Content)
return h.database.GetSQLiteQueries().CreateContentVersion(context.Background(), sqlite.CreateContentVersionParams{
ContentID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
Type: c.Type,
CreatedBy: c.LastEditedBy,
ContentID: c.ID,
SiteID: c.SiteID,
HtmlContent: c.HtmlContent,
OriginalTemplate: c.OriginalTemplate,
Type: c.Type,
CreatedBy: c.LastEditedBy,
})
case "postgresql":
c := content.(postgresql.Content)
return h.database.GetPostgreSQLQueries().CreateContentVersion(context.Background(), postgresql.CreateContentVersionParams{
ContentID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
Type: c.Type,
CreatedBy: c.LastEditedBy,
ContentID: c.ID,
SiteID: c.SiteID,
HtmlContent: c.HtmlContent,
OriginalTemplate: c.OriginalTemplate,
Type: c.Type,
CreatedBy: c.LastEditedBy,
})
}
return fmt.Errorf("unsupported database type")