Files
creator-hub/internal/creator/settings.go
T

158 lines
6.2 KiB
Go

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)
}
var runningCheckpoints int
if err := tx.QueryRowContext(ctx, `SELECT count(*) FROM creator_collection_checkpoint WHERE status='running'`).Scan(&runningCheckpoints); err != nil {
return Settings{}, databaseError(err)
}
if runningCheckpoints > 0 {
return Settings{}, ErrConflict
}
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()
start, end, err := NewCollectionWindow(now, input.LookbackDays)
if err != nil {
return Settings{}, err
}
if _, err := tx.ExecContext(ctx, `
UPDATE creator_collection_checkpoint
SET window_start=$1, window_end=$2, cursor='', lease_token='', lease_until=NULL,
status=CASE WHEN status='blocked' THEN 'blocked' ELSE 'idle' END,
last_error=CASE WHEN status='blocked' THEN last_error ELSE '' END
WHERE status <> 'running'`, start, end); err != nil {
return Settings{}, databaseError(err)
}
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
}