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 }