package creator import ( "context" "fmt" "strings" "time" ) func scanSettings(scanner interface{ Scan(...any) error }) (Settings, error) { var result Settings if err := scanner.Scan(&result.LookbackDays, &result.NewWorkIntervalSeconds, &result.MetricInitialIntervalSeconds, &result.MetricMultiplier, &result.MetricMaxIntervalSeconds, &result.MetricAgeSeconds, &result.AIProvider, &result.AIModel, &result.AIConfigured, &result.TranscriptionProvider, &result.TranscriptionModel, &result.TranscriptionConfigured, &result.UpdatedAt); err != nil { return Settings{}, err } result.UpdatedAt = result.UpdatedAt.UTC() return result, nil } const settingsSelect = `SELECT lookback_days, new_work_interval_seconds, metric_initial_interval_seconds, metric_multiplier, metric_max_interval_seconds, metric_age_seconds, ai_provider, ai_model, ai_configured, transcription_provider, transcription_model, transcription_configured, updated_at FROM creator_settings WHERE id = true` func (s *Store) GetSettings(ctx context.Context) (Settings, error) { result, err := scanSettings(s.db.QueryRowContext(ctx, settingsSelect)) return result, rowError(err) } func (s *Store) UpdateSettings(ctx context.Context, input SettingsUpdate) (Settings, error) { input.AIProvider = strings.TrimSpace(input.AIProvider) input.AIModel = strings.TrimSpace(input.AIModel) input.TranscriptionProvider = strings.TrimSpace(input.TranscriptionProvider) input.TranscriptionModel = strings.TrimSpace(input.TranscriptionModel) if err := ValidateSettings(input); err != nil { return Settings{}, err } if input.AIConfigured && (input.AIProvider == "" || input.AIModel == "") { return Settings{}, ErrInvalid } if input.TranscriptionConfigured && (input.TranscriptionProvider == "" || input.TranscriptionModel == "") { return Settings{}, ErrInvalid } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return Settings{}, fmt.Errorf("begin creator settings update: %w", err) } defer tx.Rollback() if _, err := tx.ExecContext(ctx, ` UPDATE creator_settings SET lookback_days=$1, new_work_interval_seconds=$2, metric_initial_interval_seconds=$3, metric_multiplier=$4, metric_max_interval_seconds=$5, metric_age_seconds=$6, ai_provider=$7, ai_model=$8, ai_configured=$9, transcription_provider=$10, transcription_model=$11, transcription_configured=$12, updated_at=now() WHERE id=true`, input.LookbackDays, input.NewWorkIntervalSeconds, input.MetricInitialIntervalSeconds, input.MetricMultiplier, input.MetricMaxIntervalSeconds, input.MetricAgeSeconds, input.AIProvider, input.AIModel, input.AIConfigured, input.TranscriptionProvider, input.TranscriptionModel, input.TranscriptionConfigured); err != nil { return Settings{}, databaseError(err) } rows, err := tx.QueryContext(ctx, ` SELECT p.work_id, w.published_at FROM creator_metric_plan p JOIN creator_work w ON w.id=p.work_id FOR UPDATE`) if err != nil { return Settings{}, databaseError(err) } type metricSchedule struct { workID string publishedAt *time.Time } schedules := make([]metricSchedule, 0) for rows.Next() { var item metricSchedule if err := rows.Scan(&item.workID, &item.publishedAt); err != nil { _ = rows.Close() return Settings{}, err } schedules = append(schedules, item) } if err := rows.Err(); err != nil { _ = rows.Close() return Settings{}, err } if err := rows.Close(); err != nil { return Settings{}, err } now := time.Now().UTC() for _, schedule := range schedules { nextAt, reason := NextMetricAtValue(schedule.publishedAt, now, input) stopped := nextAt.IsZero() if _, err := tx.ExecContext(ctx, ` UPDATE creator_metric_plan SET next_plan_at=$2, interval_seconds=$3, multiplier=$4, max_interval_seconds=$5, stopped=$6, stop_reason=$7, updated_at=now() WHERE work_id=$1`, schedule.workID, nullableArg(nextAt), input.MetricInitialIntervalSeconds, input.MetricMultiplier, input.MetricMaxIntervalSeconds, stopped, reason); err != nil { return Settings{}, databaseError(err) } if _, err := tx.ExecContext(ctx, ` UPDATE creator_work SET next_metric_at=$2, metric_stop_reason=$3, updated_at=now() WHERE id=$1`, schedule.workID, nullableArg(nextAt), reason); err != nil { return Settings{}, databaseError(err) } } if err := tx.Commit(); err != nil { return Settings{}, fmt.Errorf("commit creator settings update: %w", err) } return s.GetSettings(ctx) } func NextMetricAtValue(publishedAt *time.Time, now time.Time, input SettingsUpdate) (time.Time, string) { if publishedAt == nil { return time.Time{}, "published_at_pending_verification" } return NextMetricAt(publishedAt.UTC(), now.UTC(), time.Duration(input.MetricInitialIntervalSeconds)*time.Second, time.Duration(input.MetricMaxIntervalSeconds)*time.Second, input.MetricMultiplier, time.Duration(input.MetricAgeSeconds)*time.Second) } func (s *Store) SetEventDisplayed(ctx context.Context, eventID string, displayedAt time.Time) (InteractionEvent, error) { if displayedAt.IsZero() { displayedAt = time.Now().UTC() } if _, err := s.db.ExecContext(ctx, `UPDATE creator_event SET displayed_at=$2 WHERE id=$1`, eventID, displayedAt.UTC()); err != nil { return InteractionEvent{}, databaseError(err) } return s.GetEvent(ctx, eventID) } func (s *Store) EnsureSchema(ctx context.Context) error { if _, err := s.db.ExecContext(ctx, `SELECT 1 FROM creator_settings WHERE id=true`); err != nil { return fmt.Errorf("check creator schema: %w", err) } return nil }