Files
felhom.eu/hub/internal/store/household_prefs.go
T

60 lines
3.1 KiB
Go

package store
import "fmt"
// R-1199 reopened (2026-10-22). Controller 0.338.0 made a box with no locally saved notification list push its
// DEFAULT list at every start, so the hub mails by what the box's page shows. On a REBUILT box that overwrote the list
// the household had saved on its old box — measured the same morning: demo-felhom's own 13-event list, written by an
// earlier box, became the 17-event default at 07:54:55 UTC. The hub could not refuse it, because nothing in the row
// said who wrote it: the claim seeds a row (SeedNotificationPrefs), a box push upserts it (SaveNotificationPrefs), and
// a one-time migration adds event types (SeedEventTypesOnce).
//
// household_saved is that mark. It is set ONLY by a push the box declares as the household's own save (the settings
// page, or the re-push of a list saved on the box) and is never cleared; a default push never overwrites a row that
// carries it (api.handleSavePreferences). Pinned by TestR1199Reopened_* (api) and TestHouseholdSaved_* (store).
// householdSavedBackfillKey guards the one-time backfill of existing rows.
const householdSavedBackfillKey = "r1199_household_saved_backfill"
// migrateHouseholdSaved adds the column and, once, marks every existing row a box ever wrote. The rule: a row whose
// updated_at differs from its created_at was written after its creation — by a box push (a seed is an insert and
// never updates). Read on the live hub 2026-10-22 before the change: Tester-2 and Tester4 (seeded, never pushed) →
// false; demo-felhom, demo-hp, tester-1 and _resend-rotation-test (pushed) → true. Each row's verdict is logged.
// (SeedEventTypesOnce also bumps updated_at; on 2026-10-22 every row it touched had been pushed, so the rule holds for
// the rows that exist. A row created after this backfill is never judged by it.)
func (s *Store) migrateHouseholdSaved() error {
s.db.Exec(`ALTER TABLE customer_notifications ADD COLUMN household_saved INTEGER NOT NULL DEFAULT 0`)
var done string
if err := s.db.QueryRow(`SELECT value FROM hub_settings WHERE key = ?`, householdSavedBackfillKey).Scan(&done); err == nil && done != "" {
return nil
}
rows, err := s.db.Query(`SELECT customer_id, COALESCE(created_at, ''), COALESCE(updated_at, '') FROM customer_notifications`)
if err != nil {
return err
}
type r struct{ id, created, updated string }
var all []r
for rows.Next() {
var x r
if err := rows.Scan(&x.id, &x.created, &x.updated); err != nil {
rows.Close()
return err
}
all = append(all, x)
}
rows.Close()
for _, x := range all {
saved := x.created != "" && x.updated != "" && x.created != x.updated
if saved {
if _, err := s.db.Exec(`UPDATE customer_notifications SET household_saved = 1 WHERE customer_id = ?`, x.id); err != nil {
return fmt.Errorf("mark %s: %w", x.id, err)
}
}
if s.logger != nil {
s.logger.Printf("[INFO] R-1199 backfill: notification list of %s household_saved=%t (created %s, updated %s)", x.id, saved, x.created, x.updated)
}
}
_, err = s.db.Exec(`INSERT INTO hub_settings (key, value) VALUES (?, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, householdSavedBackfillKey)
return err
}