Seed household settings at boot and serve the settings API
CI / ci (pull_request) Successful in 1m31s
Independent review Codex gpt-6-sol high: APPROVED (0 findings)
CI / ci (push) Successful in 2m8s

Port of Feature 04 task 3 (7629cab) onto Feature 04 + Feature 03.

On first boot create the single household_settings row with the zone from
MEALQUEST_DEFAULT_TZ and the seed times and days (dinner 17:30, reminder
16:30, rotation check 09:00, plan draft Fri 09:00, suggestions Sun 08:00,
chat autosend Sat 08:00). An existing row is never overwritten.

Add household.SettingsService with Get, a validating Update (IANA zone,
zero-padded 24-hour HH:MM, day of week 0-6) that records updated_by, and
in-process Subscribe for the scheduler. Serve it as GET and PATCH
/api/v1/settings/household; invalid input is a 400 in the standard error
body. Migration 0002 adds household_settings.updated_by. Link time/tzdata
from internal/household so zones load in the minimal image.

Review fixes:
- F1: the settings routes are mounted on the protected /api/v1 subrouter,
  so they need a session (RequireSession) and, for PATCH, the CSRF token.
  updated_by is the session actor's subject from auth.ActorFrom; the
  duplicate internal/actor package is dropped, and Update refuses an empty
  member instead of storing NULL. A test drives PATCH through the
  production router with a real SessionManager session.
- F2: config no longer validates MEALQUEST_DEFAULT_TZ. Seeding requires a
  valid zone only when it creates the row (ErrNoDefaultZone,
  ErrInvalidDefaultZone, both naming the variable at boot), so a reboot
  over existing settings ignores a missing or invalid default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 11:42:35 -04:00
co-authored by Claude Opus 5.5
parent ea4047a852
commit c22470bf35
20 changed files with 1423 additions and 34 deletions
+15
View File
@@ -68,6 +68,16 @@ func TestLoad(t *testing.T) {
name: "all required vars present",
env: valid(),
},
{
name: "default time zone set",
env: with(map[string]string{EnvDefaultTZ: "America/New_York"}),
},
{
// Validated only when seeding the household settings, so a
// stale value cannot stop a boot that no longer uses it.
name: "default time zone not an IANA zone",
env: with(map[string]string{EnvDefaultTZ: "Mars/Olympus_Mons"}),
},
{
name: "one missing",
env: with(nil, EnvHTTPAddr),
@@ -410,6 +420,8 @@ func TestLoadValues(t *testing.T) {
EnvOIDCClientID: "mealquest",
EnvOIDCClientSecretFile: clientSecretPath,
EnvOIDCAllowedGroups: " mealquest-admin ,family",
EnvDefaultTZ: "Europe/London",
}
cfg, err := load(func(k string) (string, bool) { v, ok := env[k]; return v, ok })
if err != nil {
@@ -465,6 +477,9 @@ func TestLoadValues(t *testing.T) {
if !slices.Equal(cfg.TrustedProxy, want) {
t.Errorf("TrustedProxy = %v, want %v", cfg.TrustedProxy, want)
}
if cfg.DefaultTZ != "Europe/London" {
t.Errorf("DefaultTZ = %q, want Europe/London", cfg.DefaultTZ)
}
}
func TestSecretRedacted(t *testing.T) {