Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
package feeds
import (
"strings"
"time"
"github.com/google/uuid"
)
// PresentFeed maps an input_feeds row to the public/legacy feed DTO.
// Legacy fields (item_path, is_active, last_synced, product_count) are always set;
// v2 fields (feed_type, sync_interval_minutes, last_synced_at, options) are included for dual-support.
func PresentFeed(feed map[string]any) map[string]any {
if feed == nil {
return nil
}
opts, _ := feed["options"].(map[string]any)
if opts == nil {
opts = map[string]any{}
}
itemPath, _ := opts["item_path"].(string)
if itemPath == "" {
itemPath, _ = feed["item_path"].(string)
}
status, _ := feed["status"].(string)
lastSynced := formatAPITime(feed["last_synced_at"])
productsUpdated := formatAPITime(feed["products_updated_at"])
productCount := intFromAny(feed["product_count"])
mappingFieldCount := intFromAny(feed["mapping_field_count"])
hasMappings := mappingFieldCount > 0
if v, ok := feed["has_mappings"].(bool); ok {
hasMappings = v || mappingFieldCount > 0
}
mappingIncomplete := !hasMappings
if v, ok := feed["mapping_incomplete"].(bool); ok {
mappingIncomplete = v
}
// last_data_at: live sync timestamp when present, else latest raw product update
// (covers MySQL→Postgres imports where last_synced_at was never set).
lastDataAt := lastSynced
if lastDataAt == nil {
lastDataAt = productsUpdated
}
out := map[string]any{
"id": stringifyID(feed["id"]),
"name": feed["name"],
"url": nullish(feed["url"]),
"item_path": itemPath,
"is_active": strings.EqualFold(status, "active"),
"product_count": productCount,
"mapping_field_count": mappingFieldCount,
"has_mappings": hasMappings,
"mapping_incomplete": mappingIncomplete,
"status": status,
"last_synced": lastSynced,
"created_at": formatAPITime(feed["created_at"]),
"updated_at": formatAPITime(feed["updated_at"]),
"feed_type": feed["feed_type"],
"sync_interval_minutes": feed["sync_interval_minutes"],
"last_synced_at": lastSynced,
"products_updated_at": productsUpdated,
"last_data_at": lastDataAt,
"options": opts,
}
if deltas, ok := opts["last_sync_deltas"].(map[string]any); ok && deltas != nil {
out["last_sync_deltas"] = deltas
}
return out
}
func intFromAny(v any) int {
switch t := v.(type) {
case int:
return t
case int32:
return int(t)
case int64:
return int(t)
case float64:
return int(t)
default:
return 0
}
}
// PresentFeeds maps a page of feed rows through PresentFeed.
func PresentFeeds(items []map[string]any) []map[string]any {
out := make([]map[string]any, 0, len(items))
for _, item := range items {
out = append(out, PresentFeed(item))
}
return out
}
func stringifyID(v any) any {
switch t := v.(type) {
case uuid.UUID:
return t.String()
case [16]byte:
return uuid.UUID(t).String()
default:
return v
}
}
func nullish(v any) any {
if v == nil {
return nil
}
if s, ok := v.(string); ok && s == "" {
return nil
}
return v
}
func formatAPITime(v any) any {
if v == nil {
return nil
}
switch t := v.(type) {
case time.Time:
if t.IsZero() {
return nil
}
return t.UTC().Format(time.RFC3339)
case *time.Time:
if t == nil || t.IsZero() {
return nil
}
return t.UTC().Format(time.RFC3339)
case string:
if t == "" {
return nil
}
return t
default:
return v
}
}