Add notification preferences API and screen (1n)
CI / ci (pull_request) Successful in 6m57s
CI / ci (push) Successful in 5m49s

GET/PUT /api/v1/settings/notifications read and replace the event x
{push, chat} matrix for the five events and the reminder time. The PUT
validates every event exactly once and an HH:MM time, refuses unknown
events and fields, and writes the preferences and the household
settings' reminder_time (the one copy) in one transaction, publishing
the new settings to the scheduler as a household settings change does.

/settings/notifications renders mock 1n: the matrix with Toggle
switches, including the next-day "Keep it in rotation?" Rotation check
(D-021); the reminder time edited in a sheet; the push permission row
using pushStatus/subscribePush/unsubscribePush; the iOS InstallHint;
the Google Chat webhook card with the masked URL, "Connected · {space}"
and Send test showing success or the HTTP status; and the preview
bubble. The desktop 2e Notifications & AI tab shows the matrix and
webhook cards in its left column. The settings home row now reads Web
Push on this device and the Google Chat space, and the Household form
takes a reminder time saved elsewhere without undoing it.

Maestro task 308a460d-b22c-4179-acba-d5ea0968ccd5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 14:52:02 -04:00
co-authored by Claude Opus 5.5
parent fc5b926edf
commit fd835829e2
25 changed files with 2068 additions and 113 deletions
+61
View File
@@ -133,6 +133,39 @@ func (q *Queries) InsertNotification(ctx context.Context, arg InsertNotification
return result.RowsAffected()
}
const listNotificationPrefs = `-- name: ListNotificationPrefs :many
SELECT event, push, chat, created_at, updated_at FROM notification_prefs
`
func (q *Queries) ListNotificationPrefs(ctx context.Context) ([]NotificationPref, error) {
rows, err := q.db.QueryContext(ctx, listNotificationPrefs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []NotificationPref
for rows.Next() {
var i NotificationPref
if err := rows.Scan(
&i.Event,
&i.Push,
&i.Chat,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const markNotificationSent = `-- name: MarkNotificationSent :execrows
UPDATE notification_outbox
SET status = 'sent', attempts = attempts + 1, next_attempt_at = NULL, updated_at = ?1
@@ -200,3 +233,31 @@ func (q *Queries) RetryNotification(ctx context.Context, arg RetryNotificationPa
}
return result.RowsAffected()
}
const updateNotificationPref = `-- name: UpdateNotificationPref :execrows
UPDATE notification_prefs
SET push = ?, chat = ?, updated_at = ?
WHERE event = ?
`
type UpdateNotificationPrefParams struct {
Push int64
Chat int64
UpdatedAt string
Event string
}
// Sets which channels an event goes to; affects no row when the event has
// no preference row.
func (q *Queries) UpdateNotificationPref(ctx context.Context, arg UpdateNotificationPrefParams) (int64, error) {
result, err := q.db.ExecContext(ctx, updateNotificationPref,
arg.Push,
arg.Chat,
arg.UpdatedAt,
arg.Event,
)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
+4
View File
@@ -176,6 +176,7 @@ type Querier interface {
// Regular, and every Quest log dinner no other plan for the week starting
// from_week_start or later has on any day.
ListDraftPool(ctx context.Context, arg ListDraftPoolParams) ([]Dinner, error)
ListNotificationPrefs(ctx context.Context) ([]NotificationPref, error)
// Every name the household turned down, oldest decision first: the name
// each declined suggestion was suggested under, which renaming its dinner
// does not change, and the name each dinner a Rotation check dropped
@@ -300,6 +301,9 @@ type Querier interface {
ToggleDinnerFavorite(ctx context.Context, arg ToggleDinnerFavoriteParams) (Dinner, error)
// An unknown id returns sql.ErrNoRows.
UpdateDinner(ctx context.Context, arg UpdateDinnerParams) (Dinner, error)
// Sets which channels an event goes to; affects no row when the event has
// no preference row.
UpdateNotificationPref(ctx context.Context, arg UpdateNotificationPrefParams) (int64, error)
// Creates the single row (id 1) or replaces every column except id and
// created_at.
UpsertAISettings(ctx context.Context, arg UpsertAISettingsParams) (AiSetting, error)