65a9aae4e5
Every public method on TaskService and ResidenceService now takes ctx context.Context as the first arg and routes its repo calls through .WithContext(ctx). With otelgorm registered, this means every API endpoint backed by these two services produces a flame graph in Jaeger where the SQL spans nest under the parent HTTP request span — instead of appearing as orphaned queries. Endpoints now fully traced (HTTP → service → SQL): - GET /api/tasks/ (already shipped) - GET /api/tasks/by-residence/:id/ (already shipped) - GET /api/tasks/:id/ - POST /api/tasks/ - POST /api/tasks/bulk/ - PUT /api/tasks/:id/ - DELETE /api/tasks/:id/ - POST /api/tasks/:id/in-progress/ - POST /api/tasks/:id/cancel/ - POST /api/tasks/:id/uncancel/ - POST /api/tasks/:id/archive/ - POST /api/tasks/:id/unarchive/ - POST /api/tasks/:id/complete/ - POST /api/tasks/:id/quick-complete/ - GET /api/tasks/completions/* (CRUD) - GET /api/static_data/ (categories, priorities, frequencies) - GET /api/residences/ - GET /api/residences/my/ - GET /api/residences/summary/ - GET /api/residences/:id/ - POST /api/residences/ - PUT /api/residences/:id/ - DELETE /api/residences/:id/ - Share-code + member management endpoints - GET /api/residences/:id/report/ Mechanical work: ~50 method signatures, ~80 handler call sites, ~25 test call sites updated. Internal sendTaskCompletedNotification helper also takes ctx so background notification SQL nests correctly. The remaining services (ContractorService, DocumentService, AuthService, NotificationService, SubscriptionService) follow the same pattern; they continue to emit untraced SQL until migrated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
4.8 KiB
Go
152 lines
4.8 KiB
Go
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(c.Request().Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
taskCategories, err := h.taskService.GetCategories(c.Request().Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
taskPriorities, err := h.taskService.GetPriorities(c.Request().Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
taskFrequencies, err := h.taskService.GetFrequencies(c.Request().Context())
|
|
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",
|
|
})
|
|
}
|