Wire plan_ready, list_ready, new_suggestions and Send to Google Chat
Connect the features to the notification outbox so real events notify,
each stored by the new notify.EmitTx in the transaction of the change it
is about (Wake after commit):
- plan_ready:{weekStart} (/plan) when a draft job stores the AI's draft;
a redraft shares the key and a manual swap never emits. The title reads
"Next week's plan is ready" for the coming week.
- list_ready:{weekStart} (/plan/shopping) when a week's Supply run is
first created, by generate or after Push to Calendar.
- new_suggestions:{batchId} (/discover) when a scheduled or manual batch
ends done with cards; replenish batches never notify.
- plan.Service.EmitDayOfReminder(date) emits day_of_reminder:{date} for
the Feature 20 scheduler.
Add POST /api/v1/shopping/{weekStart}/send-chat: shopping.SendToChat posts
the dinners with needed items and a link through chat.PostSupplyRun,
records sent_to_chat_at, and is idempotent per week unless
{"resend": true}. The Supply run's Send to Google Chat button calls it
("Send again" once sent) and shows failures in an ErrorBanner; the unused
Unavailable placeholder is removed.
Task: f0cd9d50-9c11-41be-a945-7bae9f5f28a4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+33
-16
@@ -20,6 +20,7 @@ import (
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/dinners"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/jobs"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/notify"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
||||
)
|
||||
@@ -109,14 +110,16 @@ type Service struct {
|
||||
st *store.Store
|
||||
grounder *recipes.Grounder
|
||||
suggest Suggester
|
||||
notifier *notify.Service
|
||||
logger *slog.Logger
|
||||
batchJob *jobs.Kind[BatchJob]
|
||||
}
|
||||
|
||||
// NewService returns a service over st that suggests with suggest, grounds
|
||||
// with grounder and registers the KindSuggestionBatch job on runner, which
|
||||
// must not have started yet.
|
||||
func NewService(st *store.Store, grounder *recipes.Grounder, suggest Suggester, runner *jobs.Runner, logger *slog.Logger) (*Service, error) {
|
||||
// with grounder, tells the household through notifier, which must be over
|
||||
// st, and registers the KindSuggestionBatch job on runner, which must not
|
||||
// have started yet.
|
||||
func NewService(st *store.Store, grounder *recipes.Grounder, suggest Suggester, runner *jobs.Runner, notifier *notify.Service, logger *slog.Logger) (*Service, error) {
|
||||
switch {
|
||||
case st == nil:
|
||||
return nil, errors.New("suggest: nil store")
|
||||
@@ -126,10 +129,12 @@ func NewService(st *store.Store, grounder *recipes.Grounder, suggest Suggester,
|
||||
return nil, errors.New("suggest: nil suggester")
|
||||
case runner == nil:
|
||||
return nil, errors.New("suggest: nil job runner")
|
||||
case notifier == nil:
|
||||
return nil, errors.New("suggest: nil notifier")
|
||||
case logger == nil:
|
||||
return nil, errors.New("suggest: nil logger")
|
||||
}
|
||||
s := &Service{st: st, grounder: grounder, suggest: suggest, logger: logger}
|
||||
s := &Service{st: st, grounder: grounder, suggest: suggest, notifier: notifier, logger: logger}
|
||||
kind, err := jobs.Register(runner, KindSuggestionBatch, s.runBatch, Reasons...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -252,27 +257,39 @@ func (s *Service) startBatch(ctx context.Context, tx *sql.Tx, trigger string) (B
|
||||
return Batch{ID: row.ID, JobID: jobID, Trigger: row.Trigger, Status: row.Status, Created: true}, nil
|
||||
}
|
||||
|
||||
// finishBatch records running batch id as status, with
|
||||
// jobs.FinalWriteContext: the write is made even when ctx has been
|
||||
// cancelled, but a writer that stays busy cannot hold its worker past
|
||||
// jobs.FinalWriteTimeout. Its job's handler returns any error, so the job
|
||||
// finishBatch records running batch id as status, and emits ev with it
|
||||
// when ev is not nil, with jobs.FinalWriteContext: the write is made even
|
||||
// when ctx has been cancelled, but a writer that stays busy cannot hold its
|
||||
// worker past jobs.FinalWriteTimeout. Its job's handler returns any error, so the job
|
||||
// never ends succeeded with the batch still running; the runner keeps only
|
||||
// a code, so the error is logged too.
|
||||
func (s *Service) finishBatch(ctx context.Context, id, status string) error {
|
||||
func (s *Service) finishBatch(ctx context.Context, id, status string, ev *notify.Event) error {
|
||||
write, cancel := jobs.FinalWriteContext(ctx)
|
||||
defer cancel()
|
||||
n, err := db.New(s.st.Writer).FinishSuggestionBatch(write, db.FinishSuggestionBatchParams{
|
||||
Status: status,
|
||||
UpdatedAt: store.Now().String(),
|
||||
ID: id,
|
||||
err := s.st.WithTx(write, func(tx *sql.Tx) error {
|
||||
n, err := db.New(tx).FinishSuggestionBatch(write, db.FinishSuggestionBatchParams{
|
||||
Status: status,
|
||||
UpdatedAt: store.Now().String(),
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return errors.New("batch is not running")
|
||||
}
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
return s.notifier.EmitTx(write, tx, *ev)
|
||||
})
|
||||
if err == nil && n != 1 {
|
||||
err = errors.New("batch is not running")
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "record suggestion batch status", "batch_id", id, "status", status, "error", err)
|
||||
return fmt.Errorf("suggest: record batch %s %s: %w", id, status, err)
|
||||
}
|
||||
if ev != nil {
|
||||
s.notifier.Wake()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user