Feature 21: Home Dashboard (#181)
CI / ci (pull_request) Successful in 8m6s
CI / ci (push) Successful in 11m26s

Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
This commit was merged in pull request #181.
This commit is contained in:
2026-10-10 20:07:53 -04:00
committed by approver_agent
parent ecf7c6be89
commit fd087e0ba3
59 changed files with 3923 additions and 87 deletions
+63
View File
@@ -97,6 +97,22 @@ func (q *Queries) GetNotificationPref(ctx context.Context, event string) (Notifi
return i, err
}
const hasNotificationSince = `-- name: HasNotificationSince :one
SELECT EXISTS (
SELECT 1 FROM notification_outbox
WHERE CAST(?1 AS TEXT) IS NULL OR created_at > CAST(?1 AS TEXT)
) AS found
`
// Reports whether a notification was stored after since, or at all when
// since is NULL.
func (q *Queries) HasNotificationSince(ctx context.Context, since sql.NullString) (bool, error) {
row := q.db.QueryRowContext(ctx, hasNotificationSince, since)
var found bool
err := row.Scan(&found)
return found, err
}
const insertNotification = `-- name: InsertNotification :execrows
INSERT INTO notification_outbox (
id, event_key, channel, payload, status, attempts, next_attempt_at, created_at, updated_at
@@ -166,6 +182,53 @@ func (q *Queries) ListNotificationPrefs(ctx context.Context) ([]NotificationPref
return items, nil
}
const listRecentNotifications = `-- name: ListRecentNotifications :many
SELECT o.event_key, o.payload, o.created_at FROM notification_outbox o
WHERE o.id = (
SELECT i.id FROM notification_outbox i
WHERE i.event_key = o.event_key
ORDER BY i.created_at DESC, i.id DESC
LIMIT 1
)
ORDER BY o.created_at DESC, o.event_key DESC
LIMIT ?1
`
type ListRecentNotificationsRow struct {
EventKey string
Payload string
CreatedAt string
}
// Returns the last lim distinct events stored, newest first: one row per
// event key, the last stored for it whatever its channel, so an event
// stored for push and Google Chat is one row. The last, because a channel
// switched on later can store the same event key again, and
// HasNotificationSince counts that row: an event is as new as its newest
// row, so marking it seen also covers that row.
func (q *Queries) ListRecentNotifications(ctx context.Context, lim int64) ([]ListRecentNotificationsRow, error) {
rows, err := q.db.QueryContext(ctx, listRecentNotifications, lim)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListRecentNotificationsRow
for rows.Next() {
var i ListRecentNotificationsRow
if err := rows.Scan(&i.EventKey, &i.Payload, &i.CreatedAt); 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
+14
View File
@@ -182,6 +182,9 @@ type Querier interface {
// them only when a member edited them while it was grounding. Like
// GroundDinner it clears the pasted recipe_url: the recipe replaces the link.
GroundPendingDinner(ctx context.Context, arg GroundPendingDinnerParams) (int64, error)
// Reports whether a notification was stored after since, or at all when
// since is NULL.
HasNotificationSince(ctx context.Context, since sql.NullString) (bool, error)
// Stores a pending notification for one channel, due at once. A row already
// stored for the event key and channel, in any status, is left unchanged and
// no row is inserted.
@@ -212,6 +215,13 @@ type Querier interface {
// Every stored subscription with its keys, for the push sender, oldest
// first.
ListPushSubscriptions(ctx context.Context) ([]ListPushSubscriptionsRow, error)
// Returns the last lim distinct events stored, newest first: one row per
// event key, the last stored for it whatever its channel, so an event
// stored for push and Google Chat is one row. The last, because a channel
// switched on later can store the same event key again, and
// HasNotificationSince counts that row: an event is as new as its newest
// row, so marking it seen also covers that row.
ListRecentNotifications(ctx context.Context, lim int64) ([]ListRecentNotificationsRow, error)
// Reads for the AI household context block (internal/ai/harness).
// The most recent names the household turned down, newest first, as
// ListPassedNames has them.
@@ -254,6 +264,10 @@ type Querier interface {
// Records list id sent to Google Chat at sent_to_chat_at, its send
// confirmed, unless it is frozen into the Supply archive.
MarkShoppingListSentToChat(ctx context.Context, arg MarkShoppingListSentToChatParams) (int64, error)
// Moves updates_seen_at forward to seen_at; affects no row when it is
// already at or after seen_at, so it never moves back. It is not a settings
// edit: updated_by and updated_at are left as they are.
MarkUpdatesSeen(ctx context.Context, seenAt string) (int64, error)
// Records an edit of plan id, raising its revision: a synced plan becomes
// changed, any other keeps its status.
MarkWeekPlanEdited(ctx context.Context, arg MarkWeekPlanEditedParams) error
+17
View File
@@ -161,6 +161,23 @@ func (q *Queries) GetRecipeSearchSettings(ctx context.Context) (RecipeSearchSett
return i, err
}
const markUpdatesSeen = `-- name: MarkUpdatesSeen :execrows
UPDATE household_settings
SET updates_seen_at = CAST(?1 AS TEXT)
WHERE id = 1 AND (updates_seen_at IS NULL OR updates_seen_at < CAST(?1 AS TEXT))
`
// Moves updates_seen_at forward to seen_at; affects no row when it is
// already at or after seen_at, so it never moves back. It is not a settings
// edit: updated_by and updated_at are left as they are.
func (q *Queries) MarkUpdatesSeen(ctx context.Context, seenAt string) (int64, error) {
result, err := q.db.ExecContext(ctx, markUpdatesSeen, seenAt)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const recordChatTest = `-- name: RecordChatTest :execrows
UPDATE chat_settings
SET last_test_at = ?, last_test_status = ?, updated_at = ?