package api import ( "encoding/json" "net/http" ) // R-1199 reopened (2026-10-22). See store/household_prefs.go for the measured case (demo-felhom's list overwritten by // controller 0.338.0's start-up default push). Two halves live here: // // - prefsPushIsHousehold: which pushes are the household's own save. A controller ≥ 0.339.0 says so (`source`). An // older one does not: its pushes with an address or a clear are the household's (only a saved list carries // them, R-922); a push with neither is either a page save without an address or 0.338.0's default push, and the // two cannot be told apart — it is treated as NOT the household's, so it never overwrites a household's list // (the cost: on a box still on ≤ 0.338.0, a household that saves without an address does not change a list it // saved earlier, until the box takes the release that sends `source`). // - handleGetPreferences: a box with no local list reads the household's list and adopts it, instead of pushing // its default over it. Self-scoped like the save: a per-customer key reads only its own customer. // // Pinned by TestR1199Reopened_* (household_prefs_test.go). func prefsPushIsHousehold(source, email string, emailCleared bool) bool { switch source { case "household": return true case "default": return false } return email != "" || emailCleared } // handleGetPreferences answers GET /api/v1/preferences?customer_id= with the stored list and whether the // household saved it. `exists` false = no row. The address is returned only to the household's own box key (or the // global key) — the box shows it on its own page. func (h *Handler) handleGetPreferences(w http.ResponseWriter, r *http.Request) { authCustomerID, isGlobal, ok := h.checkAuthCustomer(r) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } customerID := r.URL.Query().Get("customer_id") if customerID == "" { http.Error(w, "customer_id required", http.StatusBadRequest) return } if !isGlobal && authCustomerID != customerID { http.Error(w, "Forbidden: customer_id does not match the key", http.StatusForbidden) return } prefs, err := h.store.GetNotificationPrefs(customerID) if err != nil { h.logger.Printf("[ERROR] Reading notification prefs for %s: %v", customerID, err) http.Error(w, "Internal error", http.StatusInternalServerError) return } out := struct { Exists bool `json:"exists"` HouseholdSaved bool `json:"household_saved"` Email string `json:"email"` EnabledEvents []string `json:"enabled_events"` CooldownHours int `json:"cooldown_hours"` }{EnabledEvents: []string{}} if prefs != nil { out.Exists, out.HouseholdSaved, out.Email, out.CooldownHours = true, prefs.HouseholdSaved, prefs.Email, prefs.CooldownHours if prefs.EnabledEvents != nil { out.EnabledEvents = prefs.EnabledEvents } } h.logger.Printf("[DEBUG] Notification prefs read for %s: exists=%t household_saved=%t", customerID, out.Exists, out.HouseholdSaved) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(out) }