package handlers import ( "net/http" "strings" "github.com/labstack/echo/v4" "github.com/redis/go-redis/v9" "github.com/rs/zerolog/log" "github.com/treytartt/honeydue-api/internal/dto/responses" "github.com/treytartt/honeydue-api/internal/i18n" "github.com/treytartt/honeydue-api/internal/services" ) // SeededDataResponse represents the unified seeded data response type SeededDataResponse struct { ResidenceTypes interface{} `json:"residence_types"` TaskCategories interface{} `json:"task_categories"` TaskPriorities interface{} `json:"task_priorities"` TaskFrequencies interface{} `json:"task_frequencies"` ContractorSpecialties interface{} `json:"contractor_specialties"` TaskTemplates responses.TaskTemplatesGroupedResponse `json:"task_templates"` } // StaticDataHandler handles static/lookup data endpoints type StaticDataHandler struct { residenceService *services.ResidenceService taskService *services.TaskService contractorService *services.ContractorService taskTemplateService *services.TaskTemplateService cache *services.CacheService } // NewStaticDataHandler creates a new static data handler func NewStaticDataHandler( residenceService *services.ResidenceService, taskService *services.TaskService, contractorService *services.ContractorService, taskTemplateService *services.TaskTemplateService, cache *services.CacheService, ) *StaticDataHandler { return &StaticDataHandler{ residenceService: residenceService, taskService: taskService, contractorService: contractorService, taskTemplateService: taskTemplateService, cache: cache, } } // GetStaticData handles GET /api/static_data/ // Returns all lookup/reference data in a single response with ETag support func (h *StaticDataHandler) GetStaticData(c echo.Context) error { ctx := c.Request().Context() // Check If-None-Match header for conditional request // Strip W/ prefix if present (added by reverse proxy, but we store without it) clientETag := strings.TrimPrefix(c.Request().Header.Get("If-None-Match"), "W/") // Try to get cached ETag first (fast path for 304 responses) if h.cache != nil && clientETag != "" { cachedETag, err := h.cache.GetSeededDataETag(ctx) if err == nil && cachedETag == clientETag { // Client has the latest data, return 304 Not Modified return c.NoContent(http.StatusNotModified) } } // Try to get cached seeded data if h.cache != nil { var cachedData SeededDataResponse err := h.cache.GetCachedSeededData(ctx, &cachedData) if err == nil { // Cache hit - get the ETag and return data etag, etagErr := h.cache.GetSeededDataETag(ctx) if etagErr == nil { c.Response().Header().Set("ETag", etag) c.Response().Header().Set("Cache-Control", "private, max-age=3600") } return c.JSON(http.StatusOK, cachedData) } else if err != redis.Nil { // Log cache error but continue to fetch from DB log.Warn().Err(err).Msg("Failed to get cached seeded data") } } // Cache miss - fetch all data from services residenceTypes, err := h.residenceService.GetResidenceTypes() if err != nil { return err } taskCategories, err := h.taskService.GetCategories() if err != nil { return err } taskPriorities, err := h.taskService.GetPriorities() if err != nil { return err } taskFrequencies, err := h.taskService.GetFrequencies() if err != nil { return err } contractorSpecialties, err := h.contractorService.GetSpecialties() if err != nil { return err } taskTemplates, err := h.taskTemplateService.GetGrouped() if err != nil { return err } // Build response seededData := SeededDataResponse{ ResidenceTypes: residenceTypes, TaskCategories: taskCategories, TaskPriorities: taskPriorities, TaskFrequencies: taskFrequencies, ContractorSpecialties: contractorSpecialties, TaskTemplates: taskTemplates, } // Cache the data and get ETag if h.cache != nil { etag, cacheErr := h.cache.CacheSeededData(ctx, seededData) if cacheErr != nil { log.Warn().Err(cacheErr).Msg("Failed to cache seeded data") } else { c.Response().Header().Set("ETag", etag) c.Response().Header().Set("Cache-Control", "private, max-age=3600") } } return c.JSON(http.StatusOK, seededData) } // RefreshStaticData handles POST /api/static_data/refresh/ // This is a no-op since data is fetched fresh each time // Kept for API compatibility with mobile clients func (h *StaticDataHandler) RefreshStaticData(c echo.Context) error { return c.JSON(http.StatusOK, map[string]interface{}{ "message": i18n.LocalizedMessage(c, "message.static_data_refreshed"), "status": "success", }) }